Usare Gulp per lo sviluppo di temi e plugin WordPress

Usare Gulp per sviluppare temi e plugin WordPress

Pubblicato su 10 Marzo, 2020 da Manuel Ricci in Web Development

Gulp è un toolkit JavaScript di cui ho già ampiamente discusso nella mia mini guida all’ottimizzazione dello sviluppo frontend e nel tutorial per generare il Critical CSS. Eviterò quindi di presentarlo una terza volta, se vuoi saperne di più puoi leggere i due articoli che ti ho linkato.

In questo articolo scriveremo ed analizzeremo un Gulpfile completo per lo sviluppo di temi e plugin WordPress.

Struttura delle cartelle

Partiamo da un aspetto fondamentale per far funzionare tutto a dovere, la struttura delle directory. Solitamente nei miei progetti ne adotto una simile:

Struttura cartelle iniziale

Ho volutamente omesso i vari file, perché non sono altro che file PHP utili a WordPress per mostrare correttamente le informazioni in pagina. Nell’immagine puoi osservare le cartelle inc, template-parts e languagues, a parte quest’ultima le altre due non verranno manipolate da Gulp, contengono solamente codici utili al tema. Languages, al momento vuota, conterrà successivamente il file pot per tradurre il tema in altre lingue.

Avrai già capito quindi che la cartella alla quale fare maggior attenzione è src, la quale conterrà tutti i nostri file non compilati. Essa contiene un’altra cartella, denominate assets, che a sua volta contiene tre cartelle: images, js e sass.

Ora che hai strutturato correttamente il tuo progetto possiamo focalizzarci sull’installazione dei vari packages necessari.

Nel terminale apri la directory di lavoro e digita il seguente comando

npm init

Segui le istruzioni mostrate nel prompt. Una volta inizializzato correttamente il progetto, esegui il seguente comando, sempre da terminale:

npm i --save-dev autoprefixer babel-loader babel-preset-env babel-register  @babel/core browser-sync cssnano del gulp gulp-if gulp-imagemin gulp-penthouse gulp-postcss gulp-rename gulp-replace gulp-sass gulp-sourcemaps gulp-uglify gulp-wp-pot gulp-zip vinyl-named webpack-stream yargs

Questo comando installerà una sfilza di roba. Vedrai che nella tua working directory sarà comparsa la cartella node_modules, più i file package.json e package-lock.json.

Ti ricordo che la cartella node_modules non deve essere mai trasferita, non perché ciò non è possibile, ma perché date le dimensioni è sempre meglio passare esclusivamente il file package.json ed eseguire il comando npm install che si occuperà di leggere suddetto file e di installare tutte le dipendenze del progetto.

Prima di proseguire con la creazione del file gulp. Devi sapere che scriveremo in JavaScript “moderno”, il quale deve essere compilato in JavaScript ES2015. Per il momento ti basterà creare un file .babelrc nella directory principale del progetto e scrivere quanto segue:

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

Creiamo il file gulp

Bene, ora che le dependencies sono state installate con successo è giunto il momento di creare un nuovo file nella root directory del progetto chiamato gulpfile.babel.js. Inanzitutto importiamo la dipendenza principale: gulp

import gulp from 'gulp';

Da adesso in poi quando importerò una nuova dipendenza con il comando import dovrai inserirla subito dopo la riga che hai appena scritto, così il tuo file sarà più ordinato.

Importiamo immediatamente le informazioni contenute nel file package.json, ci serviranno più tardi.

import info from "./package.json"

Il prossimo step è quello di creare una costante con i vari percorsi, i quali recupereremo di volta in volta quando ci serviranno.

const paths = {
  styles: {
    src: ['src/assets/sass/bundle.scss'],
    dest: 'dist/assets/css'
  },
  images: {
    src: 'src/assets/images/**/*.{jpg,jpeg,png,svg,gif,webp}',
    dest: 'dist/assets/images'
  },
  scripts: {
    src: ['src/assets/js/bundle.js'],
    dest: 'dist/assets/js'
  },
  other: {
    src: ['src/assets/**/*', '!src/assets/{images,js,sass}', '!src/assets/{images,js,sass}/**/*'],
    dest: 'dist/assets'
  },
  languages: {
    src: '**/*.php',
    dest: `languages/${info.name}.pot`
  },
  package: {
    src: [
      '**/*',
      '!.vscode',
      '!node_modules{,/**}',
      '!packaged{,/**}',
      '!src{,/**}',
      '!.babelrc',
      '!.gitignore',
      '!gulpfile.babel.js',
      '!package.json',
      '!package-lock.json'
    ],
    dest: 'packaged'
  }
};

Wow! Questo sì che è tanto codice… Cerchiamo di fare chiarezza.

La costante paths è un oggetto contenente varie proprietà. Tali proprietà sono a loro volta oggetti i quali contengono due proprietà srce dest, le quali rispettivamente indicano la sorgente dalla quale recuperare il file e la destinazione dove sarlvare il file processato.

In alcuni casi si può notare che la proprietà srccontiene un array (contraddistinto da parentesi quadre) ciò significa che possiamo creare più bundle contemporaneamente, ad esempio se desideriamo un file JavaScript per il frontend e uno per l’area di amministrazione.

Un’altra peculiarità la puoi trovare nella proprietà languages. Nella sua proprietà dest vengono usate le stringhe template (template literals) per interpolare in maniera più elegante la proprietà name di info, il quale ti ricordo è il nostro file package.json, importato qualche paragrafo fa. Cosa singifica? Il file che verrà generato dalla task che scriveremo, farà uso della proprietà languages di paths, che genererà un file con il nome del tuo progetto (l’hai scelto dopo aver scritto npm init nel terminale) con estensione .pot.

Ultima nota è per la proprietà package la quale contiene un array particolarmente popoloso. Il primo elemento indica tutti gli elementi presenti nella working directory comprese le cartelle, i quali verranno trasferiti nella cartella packaged, ad esclusione dei file con il punto esclamativo anteposto, tali file non sono necessari nella versione finale del tema, quindi li escludiamo dal bundle.

Task per generare il foglio di stile

La prima task che andremo a scrivere è quella necessaria a compilare i file SCSS. Importiamo quindi le varie dipendenze:

import yargs from 'yargs';
import sass from 'gulp-sass';
import gulpif from 'gulp-if';
import sourcemaps from 'gulp-sourcemaps';
import postcss from 'gulp-postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';

Le dipendenze rispettivamente servono a:

  • Prelevare argomenti dai comandi da terminale. Li andremo a implementare successivamente;
  • Compilare i file da SCSS a CSS
  • Eseguire dei metodi solo in base ad alcune specifiche condizioni
  • Generare la sourcemap. Risulterà molto utile in fase di sviluppo per individuare in quale file SCSS sono riportate le proprietà che stiamo esaminando, invece di visualizzare un anonimo file minificato.
  • Processare il CSS con i plugin configurati nella task
  • Il plugin che useremo con postcss per inserire dove necessario i prefissi alle varie proprietà CSS che ne necessitano.
  • Un altro plugin che useremo con postcss per minificare il CSS.

Terminate le importazioni creiamo una nuova costante, la quale conterrà un valore booleano in base alla presenza o meno di un argomento nel comando che inizializza la task, il quale identificherà se la compilazione è per l’ambiente di sviluppo o di produzione.

const PRODUCTION = yargs.argv.prod;

Ora che c’è tutto possiamo procedere a scrivere la task.

export const styles = () => {
  let processors = [
    autoprefixer,
  ];
  if (PRODUCTION) processors.push(cssnano);
  return gulp
    .src(paths.styles.src)
    .pipe(gulpif(!PRODUCTION, sourcemaps.init()))
    .pipe(sass().on('error', sass.logError))
    .pipe(postcss(processors))
    .pipe(gulpif(!PRODUCTION, sourcemaps.write()))
    .pipe(gulp.dest(paths.styles.dest))
};

Per prima cosa definiamo un’array di processori (plugin) che daremo in pasto a postcssquando sarà il momento. Di base c’è autoprefixer, se la costante PRODUCTION è vera, verrà aggiunto anche cssnano per la minificazione.

Dopo di che recuperiamo il file SCSS da processare (ricorda che può essere anche un array). Eseguiamo quindi i seguenti passaggi:

  • Se non siamo in produzione, inizia la generazione della sourcemap
  • Compila i file SCSS
  • Processa il codice CSS generato con postcss e i relativi plugin
  • Se non siamo in produzione, scrivi la sourcemap
  • Salva il file compilato nella destinazione specificata nella proprietà dell’oggetto styles, a sua volta proprietà della costante paths.

In dest riporto la cartella dist, la quale non esiste. Verrà creata in automatico dallo script.

Task per processare le immagini

La prossima task serve a minimizzare le immagini, allegerendole. Prima di tutto importiamo la dipendenza:

import imagemin from 'gulp-imagemin';

Scriviamo quindi la task:

export const images = () => {
  return gulp
    .src(paths.images.src)
    .pipe(gulpif(PRODUCTION, imagemin()))
    .pipe(gulp.dest(paths.images.dest));
};

Se hai capito il meccanismo prima, qui è ancora più semplice. Ogni immagine, se la costante PRODUCTION è uguale a true, viene minificata e salvata nel percorso di destinazione. Nella nostra costante paths ho definito i seguenti formati jpg, jpeg, png, svg, gif, webp. Se te ne servono altri aggiungili.

Task per processare JavaScript

Forse la task più complessa di tutto il file, ma la spunteremo anche stavolta.

Come sempre importiamo le dipendenze:

import webpack from 'webpack-stream';
import uglify from 'gulp-uglify';

e scriviamo la task:

export const scripts = () => {
  return gulp
    .src(paths.scripts.src)
    .pipe(named())
    .pipe(
      webpack({
        mode: !PRODUCTION ? 'development' : 'production',
        module: {
          rules: [
            {
              test: /\.js$/,
              use: {
                loader: 'babel-loader',
                options: {
                  presets: ['babel-preset-env']
                }
              }
            }
          ]
        },
        output: {
          filename: '[name].js'
        },
        externals: {
          jquery: 'jQuery'
        },
        devtool: !PRODUCTION ? 'inline-source-map' : false
      })
    )
    .pipe(gulpif(PRODUCTION, uglify()))
    .pipe(gulp.dest(paths.scripts.dest));
};

Non è niente di impossibile, solo un mucchio di parentesi da aprire e chiudere. Analizziamola con ordine:

  • Vengono recuperati i file JavaScript da compilare
  • Viene eseguito webpack, noto module bundler, il quale tradurrà i file ES2019 in ES2015 grazie al plugin Babel
    • Si esplicita il nome del file di output che manterrà lo stesso nome del file originale
    • Si fa presente che come libreria esterna si usa jQuery (ancora molto utilizzata da WordPress)
    • Nel caso non ci trovassimo in produzione verrà generata una sourcemap
  • Se siamo in produzione il file appena compilato verrà minificato
  • Il file compilato è pronto e viene salvato nella cartella di destinazione dei file JavaScript.

Easy, no?

Task per creare i file di traduzione

La vera peculiriatà di questo file è proprio questa task che scansiona i file PHP alla ricerca di tutte le stringhe di testo hardcoded, dove si fa uso delle funzioni di WordPress __(), _e(), _x(), ecc.

Come sempre, prima le dipendenze:

import wpPot from 'gulp-wp-pot';

e poi la task:

export const pot = () => {
  return gulp
    .src('**/*.php')
    .pipe(
      wpPot({
        domain: `${info.name}`,
        package: info.name
      })
    )
    .pipe(gulp.dest(`languages/${info.name}.pot`));
};

Cosa succede? Niente di più semplice:

  • Tutti i file PHP vengono sottoposti a scansione
  • Vengono individuate le stringhe con dominio uguale al nome del progetto
  • Viene generato il file potnella cartella languagesdel progetto.

Cosa farsene di quel file .pot? Tramite software come Poedit potrai tradurre quelle stringhe in altre lingue, i file generati dal programma dovranno essere salvati nella cartella languages e WordPress si occuperà del resto. Per ulteriori informazioni dai un’occhiata alla sezione dedicata in documentazione.

Altre task utili

Le task principali sono state analizzate, ci sono comunque altre task che possono essere incluse e le vediamo qui di seguito.

Watch

export const watch = () => {
  gulp.watch('src/assets/sass/**/*.scss', styles);
  gulp.watch('src/assets/js/**/*.js', gulp.series(scripts, reload));
  gulp.watch('**/*.php', reload);
  gulp.watch(paths.images.src, gulp.series(images, reload));
  gulp.watch(paths.other.src, gulp.series(copy, reload));
};

Nessuna dipendenza a sto giro, a parte ovviamente gulp, che abbiamo già importato all’inizio.

La task non farà altro che osservare eventuali cambiamenti nei vari file specificati nel primo parametro del metodo watch e qualora si verificassero, eseguirà la funzione o le funzioni definite nel secondo parametro.

Se osservi con attenzione noterai che due funzioni non esistono ancora nel nostro gulp file. Hai capito quali?

Serve e Reload

La prima funzione che chiamiamo in watch è reload, la quale va in coppia con serve. Cosa fanno? Evitano di farci pigiare ogni volta F5 e ci servono il sito sulla porta 3000 così da poter svolgere il debug anche via dispositivo mobile.

Importiamo le dipenze:

import browserSync from 'browser-sync'

Successivamente creiamo una costante che inizializzerà il tutto:

const server = browserSync.create();

Creiamo le due task

export const serve = done => {
  server.init({
    proxy: `localhost/${info.name}`
  });
  done();
};
export const reload = done => {
  server.reload('');
  done();
};

Servesi occuperà della creazione del local server. Occhio! Sono comunque necessari software come XAMPP per l’esecuzione di PHP e di WordPress in generale. Mentre reload, beh penso sia chiaro il suo compito.

Dopo la creazione di queste due task è possibile modificare la task degli stili aggiungendo questa riga di codice subito dopo l’ultima pipe(), integrandola con browserSync.

.pipe(server.stream());

Copy

L’altra task presente in Watch, ma non ancora implementata è copy. Il suo compito è quello di copiare i file da una directory all’altra.

Nessuna dipendenza, solo la task:

export const copy = () => {
  return gulp.src(paths.other.src).pipe(gulp.dest(paths.other.dest));
};

Critical CSS

Ci ho scritto un articolo a riguardo, te la riporto qui di seguito per completezza, ma per saperne di più vai a leggerti l’articolo.

export const critical = () => {
  return gulp
    .src(`${paths.styles.dest}/bundle.css`)
    .pipe(gulpCriticalCss({
      out: 'critical.php',
      url: `http://localhost/${info.name}`,
      width: 1400,
      height: 900,
      userAgent: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
    }));
};

Compress

Questa task è la mia preferita. In pratica crea una cartella zippata con tutti i file del tema minificati e pronti per la produzione, ma non solo. Ricordi che la task per la generazione del file pot cerca tutte le stringhe con il text domain uguale al nome del progetto? Per poter riutilizzare questo file in altri progetti, puoi scrivere come dominio di testo nei tuoi file php, una stringa tipo ‘_themename’ e farla sostituire a gulp in fase di compilazione. Fico no?

Importiamo le dipendenze:

import zip from 'gulp-zip';
import replace from 'gulp-replace';

E scriviamo la task:

export const compress = () => {
  return gulp
    .src(paths.package.src)
    .pipe(replace('_themename', info.name))
    .pipe(zip(`${info.name}.zip`))
    .pipe(gulp.dest(paths.package.dest));
};

Il comportamento è proprio quello riportato qualche riga fa. Sostituisce tutti i text domain placeholder con il nome del progetto, dopo di che zippa tutto e salva nella cartella indicata.

Clean

Prima di rigenerare i file CSS, JS e copiare i vari file necessari è sempre buona cosa eliminare la cartella distprima di farla ricreare allo script. La task clean fa proprio questo.

Importiamo la dipendeza:

import del from 'del';

e scriviamo la task:

export const clean = () => del(['dist']);

Semplice semplice.

Ultimi ritocchi

Ci siamo quasi, dobbiamo definire le task che eseguiranno in serie le varie task, così da non doverle eseguire manualmente una per volta. Ne predisporremo tre: una per lo sviluppo, una che compili tutto il progetto e una che compili il progetto e che crei anche la cartellina zippata. Useremo i metodi series e parallel di gulp, quindi niente nuove dipendenze da importare.

export const dev = gulp.series(clean, gulp.parallel(styles, scripts, images, copy), serve, watch);
export const build = gulp.series(clean, gulp.parallel(styles, scripts, images, copy, pot, critical));
export const bundle = gulp.series(build, compress);

export default dev;

L’ultima riga indica che la task di default è dev, quella destinata allo sviluppo.

Il tocco finale lo diamo modificando il file package.json dove alla proprietà scripts, elimineremo ciò che è stato scritto di default e lo sostituiremo con quanto segue:

"scripts": {
   "starts": "gulp",
   "build": "gulp build --prod",
   "bundle": "gulp bundle --prod"
 }

Salvato il file, potremo scrivere a terminale:

  • npm start per eseguire le task dev,
  • npm build per eseguire le task di build, impostando la costante PRODUCTION a vero
  • npm bundle per eseguire le task di build, più la compressione nella cartella zip e impostando la costante PRODUCTION a vero.

Per completezza ecco il file completo:

import gulp from 'gulp';
import yargs from 'yargs';
import sass from 'gulp-sass';
import gulpif from 'gulp-if';
import sourcemaps from 'gulp-sourcemaps';
import imagemin from 'gulp-imagemin';
import del from 'del';
import webpack from 'webpack-stream';
import uglify from 'gulp-uglify';
import named from 'vinyl-named';
import browserSync from 'browser-sync';
import zip from 'gulp-zip';
import replace from 'gulp-replace';
import wpPot from 'gulp-wp-pot';
import gulpCriticalCss from 'gulp-penthouse';
import postcss from 'gulp-postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
import info from './package.json';
const server = browserSync.create();
const PRODUCTION = yargs.argv.prod;
const paths = {
  styles: {
    src: ['src/assets/sass/bundle.scss'],
    dest: 'dist/assets/css'
  },
  images: {
    src: 'src/assets/images/**/*.{jpg,jpeg,png,svg,gif,webp}',
    dest: 'dist/assets/images'
  },
  scripts: {
    src: ['src/assets/js/bundle.js'],
    dest: 'dist/assets/js'
  },
  other: {
    src: ['src/assets/**/*', '!src/assets/{images,js,sass}', '!src/assets/{images,js,sass}/**/*'],
    dest: 'dist/assets'
  },
  languages: {
    src: '**/*.php',
    dest: `languages/${info.name}.pot`
  },
  package: {
    src: [
      '**/*',
      '!.vscode',
      '!node_modules{,/**}',
      '!packaged{,/**}',
      '!src{,/**}',
      '!.babelrc',
      '!.gitignore',
      '!gulpfile.babel.js',
      '!package.json',
      '!package-lock.json'
    ],
    dest: 'packaged'
  }
};
export const compress = () => {
  return gulp
    .src(paths.package.src)
    .pipe(replace('_themename', info.name))
    .pipe(zip(`${info.name}.zip`))
    .pipe(gulp.dest(paths.package.dest));
};
export const critical = () => {
  return gulp
    .src(`${paths.styles.dest}/bundle.css`)
    .pipe(gulpCriticalCss({
      out: 'critical.php',
      url: `http://localhost/${info.name}`,
      width: 1400,
      height: 900,
      userAgent: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
    }));
};
export const serve = done => {
  server.init({
    proxy: `localhost/${info.name}`
  });
  done();
};
export const reload = done => {
  server.reload('');
  done();
};
export const clean = () => del(['dist']);
export const styles = () => {
  let processors = [
    autoprefixer,
  ];
  if (PRODUCTION) processors.push(cssnano);
  return gulp
    .src(paths.styles.src)
    .pipe(gulpif(!PRODUCTION, sourcemaps.init()))
    .pipe(sass().on('error', sass.logError))
    .pipe(postcss(processors))
    .pipe(gulpif(!PRODUCTION, sourcemaps.write()))
    .pipe(gulp.dest(paths.styles.dest))
    .pipe(server.stream());
};
export const images = () => {
  return gulp
    .src(paths.images.src)
    .pipe(gulpif(PRODUCTION, imagemin()))
    .pipe(gulp.dest(paths.images.dest));
};
export const watch = () => {
  gulp.watch('src/assets/sass/**/*.scss', styles);
  gulp.watch('src/assets/js/**/*.js', gulp.series(scripts, reload));
  gulp.watch('**/*.php', reload);
  gulp.watch(paths.images.src, gulp.series(images, reload));
  gulp.watch(paths.other.src, gulp.series(copy, reload));
};
export const copy = () => {
  return gulp.src(paths.other.src).pipe(gulp.dest(paths.other.dest));
};
export const scripts = () => {
  return gulp
    .src(paths.scripts.src)
    .pipe(named())
    .pipe(
      webpack({
        mode: !PRODUCTION ? 'development' : 'production',
        module: {
          rules: [
            {
              test: /\.js$/,
              use: {
                loader: 'babel-loader',
                options: {
                  presets: ['babel-preset-env']
                }
              }
            }
          ]
        },
        output: {
          filename: '[name].js'
        },
        externals: {
          jquery: 'jQuery'
        },
        devtool: !PRODUCTION ? 'inline-source-map' : false
      })
    )
    .pipe(gulpif(PRODUCTION, uglify()))
    .pipe(gulp.dest(paths.scripts.dest));
};
export const pot = () => {
  return gulp
    .src('**/*.php')
    .pipe(
      wpPot({
        domain: `${info.name}`,
        package: info.name
      })
    )
    .pipe(gulp.dest(`languages/${info.name}.pot`));
};

export const dev = gulp.series(clean, gulp.parallel(styles, scripts, images, copy), serve, watch);
export const build = gulp.series(clean, gulp.parallel(styles, scripts, images, copy, pot, critical));
export const bundle = gulp.series(build, compress);

export default dev;

Decisamente un tutorial impegnativo. Se sei arrivato fino a qui, grazie. Se hai dubbi o domande, non devi fare altro che scrivere un commento.

Alla prossima.