Configurar Gulp para proyectos con WordPress

WordPress sigue siendo el CMS por excelencia para crear contenido en Internet. Sin embargo, WordPress carece de herramientas de forma nativa que nos permitan realizar tareas de gestión archivos, compilar ficheros Sass, minimizar CSS y/o JavaScript, etc (sin contar con plugins). Para ayudar en estas tareas podemos optar por un “module bundler” como Webpack o utilizar algún “task runner” como son los conocidos Grunt o Gulp.

En mis inicios utilizaba Grunt, pero desde hace algunos años ya me pasé a Gulp y para esta ocasión he optado por configurar un fichero Gulp en detrimento de Grunt por una mera cuestión de rendimiento, tal y como se detalla en este artículo de Cody Arsenault, en el que se comparan ambas herramientas de automatización de tareas.

Requisitos previos

En las últimas versiones de Node.js, NPM ya viene incluído.

Configuración inicial del entorno

Sistema de directorios

En este proceso voy a asumir que vamos a trabajar con un “Theme” WordPress limpio y que posee los requisitos mínimos para ser configurado, es decir, los ficheros index.phpstyle.css. Si no es así, y estás utilizando un “Theme” con muchas dependencias, puede que no obtengas los resultados esperados.

La estructura de directorios será algo parecido a lo siguiente:

wordpressthemefolder/
├── index.php
├── style.css
├── gulpfile.js
└── src/
    ├── images/
    │   └── logo.jpg
    ├── js/
    │   ├── components/
    │   │   └── utils.js
    │   └── scripts.js
    └── scss/
        ├── components/
        │   └── website.scss
        └── style.scss

Por lo tanto tendremos que crear el directorio src y en su interior los directorios: images, js, scss.

Identificación de Tareas

  • Tarea de estilos
  • Tarea de imágenes
  • Tarea de scripts (ES6)
  • Tarea de vigilancia
  • Tarea de copiado de ficheros
  • Tarea de limpieza
  • Tarea de empaquetado (Zip)
  • Composición de tareas

Inicialización del proyecto

Doy por asumido que tanto Node.js como NPM se encuentran instalados y disponibles desde el terminal. Empezaremos inicializando nuestro nuevo proyecto con el siguiente comando desde el directorio de nuestro Theme en WordPress:

npm init -y

Es la forma automatizada de crear nuestro fichero package.json con la información de nuestro proyecto. Puedes cambiar alguna de sus propiedades en cualquier momento. En un principio nos vale perfectamente hacerlo así, posteriormente cambiaremos algunas cosas del mismo.

Instalación de dependencias

Nuestra primera dependencia de desarrollo obviamente será Gulp. Para ello lanzaremos el siguiente comando desde el terminal con el que instalaremos la dependencia exacta (esto evitará actualizaciones automáticas).

npm install --save-dev -E gulp

Gulpfile con soporte ES6

Si no necesitas ofrecer compatibilidad con ES6 para IE11, puedes saltarte este paso.

Lo primero que debemos hacer para usar la sintaxis ES6 será reemplazar el fichero gulpfile.js por el fichero gulpfile.babel.js, de esta forma podremos trabajar con Babel, para que pueda recompilar nuestro código ES6 a ES5 ofreciendo compatibilidad con navegadores que no soporten ES6, como puede ser IE11.

Instalaremos las dependencias desde el terminal

npm install --save-dev @babel/register @babel/preset-env @babel/core

Una vez actualizadas nuestras dependencias, crearemos un fichero .babelrc en el que escribiremos lo siguiente:

{ "presets": ["@babel/preset-env"] }

Modo desarrollo y modo producción

Algunas de las tareas que vamos a configurar o algunos de sus procesos internos, no necesitan realizarse a menos que vayamos a desplegar en producción (Production Mode), por lo tanto es conveniente poder distinguir entre ambos modos. Para ello pasaremos una variables de entorno “production” cuando lancemos nuestros scripts.

Vamos a añadir lo siguiente en la zona “scripts” de nuestro fichero package.json:

  ...
  "scripts": {
    "start": "gulp",
    "build": "SET NODE_ENV=production& gulp build",
    "bundle": "SET NODE_ENV=production& gulp bundle"
  },
  ...

Posteriormente veremos cómo hacer uso de esta variable de entorno en el fichero gulpfile.js.

Creación de tareas con Gulp

Para permutar entre producción y desarrollo tenemos que trabajar con un condicional. Para ello necesitamos instalar gulp-if de la siguiente manera:

npm install --save-dev -E gulp-if

Una vez instalado lo importamos en gulpfile.js, también añadimos la variable que recogerá la variable de entorno de la siguiente manera:

import gulpif from 'gulp-if';

// Flag for Production Mode
const PRO = process.env.NODE_ENV === 'production' || false;

Con esto, ya podemos comenzar a crear nuestras taeras.

Tarea de estilos

Funcionalidad:

  • Compilar los estilos (Sass a CSS) del fichero style.scss y generar el fichero style.css.
  • Incorporar Autoprefixer con PostCSS.
  • Minifica el fichero compilado CSS (PRO).
  • Generar sourcemaps (!PRO).

Dependencias:

npm install --save-dev -E gulp-sass gulp-sourcemaps gulp-clean-css gulp-postcss autoprefixer
  • gulp-sass: compila ficheros sass/scss a css.
  • gulp-sourcemaps: permite averiguar el fichero origen de una propiedad CSS aplicada.
  • gulp-clean-css:  minifica el fichero CSS y otras funcionalidades.
  • gulp-postcss: permite utilizar otros plugins como Autoprefixer.
  • autoprefixer: permite incorporar de forma automática “vendor-prefixes”.

Código:

// Paquetes para Tarea de estilos
import sass from 'gulp-sass';
import sourcemaps from 'gulp-sourcemaps';
import cleanCss from 'gulp-clean-css';
import postCss from 'gulp-clean-css';
import autoprefixer from 'autoprefixer';

// Tarea de estilos ------------------------------------------------------------
export const taskStyles = () => {    
    return src('src/scss/style.scss')
    .pipe(gulpif(!PRO, sourcemaps.init()))
    .pipe(sass().on('error', sass.logError))
    .pipe(postCss([ autoprefixer({ grid: true }) ]))
    .pipe(gulpif(PRO, cleanCss({ debug: true }, (details) => {
        console.log(`${details.name}: ${details.stats.originalSize}`);
        console.log(`${details.name}: ${details.stats.minifiedSize}`);        
    })))
    .pipe(gulpif(!PRO, sourcemaps.write()))
    .pipe(dest('./'))
}

Tarea de imágenes

Funcionalidad:

  • Minifica los principales formatos de imágenes y los copia a la carpeta /dist/images (PRO)

Dependencias:

npm install --save-dev -E gulp-imagemin
  • gulp-imagemin: Minifica ficheros .png, .jpeg, .gif y .svg.

Código:

// Paqueres para Tarea de imágenes
import imagemin from 'gulp-imagemin';

// Tarea de imágenes -----------------------------------------------------------
export const taskImages = () => {
    return src('src/images/**/*.{jpg,jpeg,png,svg,gif}')
    .pipe(gulpif(PRO, imagemin([
        imagemin.gifsicle({interlaced: true}),
        imagemin.mozjpeg({quality: 90, progressive: true}),
        imagemin.optipng({optimizationLevel: 5}),     
    ],{
        verbose: true
    })))
    .pipe(dest('dist/images'));    
}

Tarea de scripts (ES6)

Funcionalidad:

  • Utiliza Babel (babel-loader) de Webpack para compilar los scripts ES6 a ES5.
  • Genera sourcemaps si está en modo desarrollo. (!PRO)
  • Unifica todos los scripts en único fichero “bundle.js”.
  • Elimina todos los “console” que pueda encontrar (PRO).

Dependencias:

npm install --save-dev -E webpack-stream babel-loader gulp-uglify
  • webpack-stream: permite trabajar con Webpack dentro de Gulp y utilizar Babel.
  • babel-loader: permite trabajar con Babel en Webpack.
  • gulp-uglify: minifica JavaScript.

Código:

// Paquetes para Tarea de scripts
import webpack from 'webpack-stream';
import uglify from 'gulp-uglify';

// Tarea de scripts (ES6) ------------------------------------------------------
export const taskScripts = () => {
    return src(['src/js/scripts.js'])
    .pipe(webpack({
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: []
                        }
                    }
                }
            ]
        },
        mode: 'none',
        devtool: !PRO ? 'inline-source-map' : false,
        output: {
            filename: 'bundle.js'
        },
    }))
    .pipe(gulpif(PRO, uglify({ 
        compress: {
            drop_console: true
        }
    })))
    .pipe(dest('dist/js'));    
}

Tarea de vigilancia

Funcionalidad:

  • Seguimiento de los cambios en ficheros de estilo .scss
  • Seguimiento de los cambios en ficheros de imágenes .png, .jpeg, .gif y .svg.
  • Seguimiento de los cambios en ficheros de scripts .js
  • Seguimiento de los cambios en ficheros contenido en la carpeta /src.

Dependencias:

  • No tiene dependencias. Haremos uso de “watch” para Gulp.

Código:

import { src, dest, watch } from 'gulp';

// Tarea de vigilancia ---------------------------------------------------------
export const whatchForChanges = () => {
    watch('src/css/**/*.scss', taskStyles);
    watch('src/images/**/*.{jpg,jpeg,png,svg,gif}', taskImages);
    watch(['src/**/*','!src/{images,js,css}','!src/{images,js,css}/**/*'], taskCopy);
    watch('src/js/**/*.js', taskScripts);
}

Tarea de copiado de ficheros

Funcionalidad:

  • Copiará todos aquellos ficheros que se encuentren en la carpeta y subcarpetas de “src”, salvo aquellos ficheros que se encuentren en las carpetas “images”, “js”“css” ya que las tareas encargadas para este tipo de ficheros ya realizan un copiado de los mismos.

Dependencias:

  • No tiene dependencias. Haremos uso de “dest” para Gulp.

Código:

import { src, dest, watch } from 'gulp';

// Tarea de copiado de ficheros ------------------------------------------------
export const taskCopy = () => {
    return src(['src/**/*','!src/{images,js,css}','!src/{images,js,css}/**/*'])
    .pipe(dest('dist'));
}

Tarea de limpieza

Funcionalidad:

  • Elimina los directorios /dist y /bundled.

Dependencias:

npm install --save-dev -E del
  • del: permite eliminar múltiples ficheros y directorios.

Código:

// Paquetes pra Tarea de limpieza
import del from 'del';

// Tarea de limpieza -----------------------------------------------------------
export const taskClean = () => del(['dist', 'bundled']);

Tarea de empaquetado ZIP

Funcionalidad:

  • Crea un fichero ZIP que nos permitirá importarlo a WordPress.

Dependencias:

npm install --save-dev -E gulp-zip
  • gulp-zip: Comprime ficheros y directorios a formato ZIP.
  • info: Extraemos la información del fichero package.json para averiguar el nombre del proyecto.

Código:

// Paquetes para Tarea de empaquetado ZIP
import zip from 'gulp-zip';
import info from "./package.json";

// Tarea de empaquetado ZIP
export const taskCompress = () => {
    return src([
        '**/*',
        '!node_modules{,/**}',
        '!bundled{,/**}',
        '!src{,/**}',
        '!.babelrc',
        '!.gitignore',
        '!gulpfile.babel.js',
        '!package.json',
        '!package-lock.json',
        ])
        .pipe(zip(`${info.name}.zip`))
        .pipe(dest('bundled'));        
};

Composición de lanzadores

Funcionalidad:

Llegados a este punto debemos poder hacer uso de estas tareas disponibles en función de si estamos trabajando en modo desarrollo o si vamos a producción. Para ello vamos a crear una serie de lanzadores que se compondrán de las tareas realizadas anteriormente. Básicamente tendremos tres tipos:

  • dev: será el modo desarrollo con el que trabajaremos habitualmente.
  • build: cuando queramos hacer un “deploy”.
  • bundle: básicamente es un build, pero empaquetando el contenido en un fichero ZIP.

Por defecto, nuestro fichero Gulp trabajará en modo dev si no se le pasa ningún parámetro.

Dependencias:

  • No tiene dependencias. Haremos uso de “series” y “parallel” de Gulp.

Código:

import { src , dest, watch, series, parallel } from 'gulp'

// Lanzadores ------------------------------------------------------------------
export const dev = series(taskClean, parallel(taskStyles, taskImages, taskCopy, taskScripts), taskWatch);
export const build = series(taskClean, parallel(taskStyles, taskImages, taskCopy, taskScripts));
export const bundle = series(taskClean, parallel(taskStyles, taskImages, taskCopy, taskScripts), taskCompress);

export default dev;

Fichero de configuración gulpfile.babel.js

import { src , dest, watch, series, parallel } from 'gulp'
import gulpif from 'gulp-if';

// Paquetes para Tarea de estilos
import sass from 'gulp-sass';
import sourcemaps from 'gulp-sourcemaps';
import cleanCss from 'gulp-clean-css';
import postCss from 'gulp-clean-css';
import autoprefixer from 'autoprefixer';

// Paquetes para Tarea de imágenes
import imagemin from 'gulp-imagemin';

// Paquetes para Tarea de scripts
import webpack from 'webpack-stream';
import uglify from 'gulp-uglify';

// Paquetes pra Tarea de limpieza
import del from 'del';

// Paquetes para Tarea de empaquetado ZIP
import zip from 'gulp-zip';
import info from "./package.json";


// Flag for Production Mode
const PRO = process.env.NODE_ENV === 'production' || false;


// Tarea de estilos ------------------------------------------------------------
export const taskStyles = () => {    
    return src('src/scss/style.scss')
    .pipe(gulpif(!PRO, sourcemaps.init()))
    .pipe(sass().on('error', sass.logError))
    .pipe(postCss([ autoprefixer({ grid: true }) ]))
    .pipe(gulpif(PRO, cleanCss({ debug: true }, (details) => {
        console.log(`${details.name}: ${details.stats.originalSize}`);
        console.log(`${details.name}: ${details.stats.minifiedSize}`);        
    })))
    .pipe(gulpif(!PRO, sourcemaps.write()))
    .pipe(dest('./'))
}

// Tarea de imágenes -----------------------------------------------------------
export const taskImages = () => {
    return src('src/images/**/*.{jpg,jpeg,png,svg,gif}')
    .pipe(gulpif(PRO, imagemin([
        imagemin.gifsicle({interlaced: true}),
        imagemin.mozjpeg({quality: 90, progressive: true}),
        imagemin.optipng({optimizationLevel: 5}),     
    ],{
        verbose: true
    })))
    .pipe(dest('dist/images'));    
}

// Tarea de scripts (ES6) ------------------------------------------------------
export const taskScripts = () => {
    return src(['src/js/scripts.js'])
    .pipe(webpack({
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: []
                        }
                    }
                }
            ]
        },
        mode: 'none',
        devtool: !PRO ? 'inline-source-map' : false,
        output: {
            filename: 'bundle.js'
        },
    }))
    .pipe(gulpif(PRO, uglify({ 
        compress: {
            drop_console: true
        }
    })))
    .pipe(dest('dist/js'));    
}

// Tarea de vigilancia ---------------------------------------------------------
export const taskWatch = () => {
    watch('src/css/**/*.scss', taskStyles);
    watch('src/images/**/*.{jpg,png,svg,gif}', taskImages);
    watch(['src/**/*','!src/{images,js,css}','!src/{images,js,css}/**/*'], taskCopy);
    watch('src/js/**/*.js', taskScripts);
}


// Tarea de copiado de ficheros ------------------------------------------------
export const taskCopy = () => {
    return src(['src/**/*','!src/{images,js,css}','!src/{images,js,css}/**/*'])
    .pipe(dest('dist'));
}

// Tarea de limpieza -----------------------------------------------------------
export const taskClean = () => del(['dist', 'bundled']);

// Tarea de empaquetado ZIP
export const taskCompress = () => {
    return src([
        '**/*',
        '!node_modules{,/**}',
        '!bundled{,/**}',
        '!src{,/**}',
        '!.babelrc',
        '!.gitignore',
        '!gulpfile.babel.js',
        '!package.json',
        '!package-lock.json',
        ])
        .pipe(zip(`${info.name}.zip`))
        .pipe(dest('bundled'));        
};

// Lanzadores ------------------------------------------------------------------
export const dev = series(taskClean, parallel(taskStyles, taskImages, taskCopy, taskScripts), taskWatch);
export const build = series(taskClean, parallel(taskStyles, taskImages, taskCopy, taskScripts));
export const bundle = series(taskClean, parallel(taskStyles, taskImages, taskCopy, taskScripts), taskCompress);

export default dev;

 

Referencias:

Gulp for WordPress: Initial Setup