When I post a link to one of my articles on social media, I try to always upload a related image to go along with it. I’ve been told these increase engagement. Up until now, this has been a manul process. I decided to search for a method to generate feature images for my posts, the way Dev.to generates feature images for their posts. I’ve found one and it uses Gulp and Puppeteer.

My journey into Gulp started backwards (ie. I found a Gulp/Puppeteer solution to my feature image generation problem). Nic Raboy wrote an article about this very topic late last year. I decided to take the solution, deconstruct it, and determine how Gulp makes all of this possible. I then wanted to incorporate this into my website’s GitHub Actions-based build pipeline.

First, let’s start with Gulp.

What is Gulp

Gulp is a framework for automating tasks in your development flow.

Formally, Gulp is:

gulp is a toolkit for automating painful or time-consuming tasks in your development workflow, so you can stop messing around and build something.

A Gulp script is comprised of Tasks. A Task is an asynchronous JavaScript function. You get to define the function. Your function should expect a callback or it should return a stream, promise, observable, etc.

Here is a very simple Gulp script. It defines two public tasks that log something to the command line. They are classified as public because they are exported.

const gulp = require('gulp');

function helloWorld(cb) {
    console.log('hello world');
    cb();
}

function helloWorldPromise() {
    return new Promise((resolve, reject) => {
        console.log('hello world promise');
        resolve();
    });
}

exports.helloWorld = helloWorld;
exports.helloWorldPromise = helloWorldPromise;

After installing Gulp (npm i gulp -g) and setting up my project NPM, running:

gulp helloWorld

Yields:

[13:58:05] Using gulpfile ~/dev/hello-gulp/gulpfile.js
[13:58:05] Starting 'helloWorld'...
hello world
[13:58:05] Finished 'helloWorld' after 2.94 ms

You can define a default task by appending exports.default = helloWorld; to the end.

You can run your default Gulp task by running gulp at the command line, in your project. For the code from above, this yields:

[14:08:08] Using gulpfile ~/dev/hello-gulp/gulpfile.js
[14:08:08] Starting 'default'...
hello world
[14:08:08] Finished 'default' after 5.04 ms

You don’t have to export all of your Gulp tasks. You can have several private gulp tasks (not exported) that you can compose to build a larger tasks with series and parallel.

For example, I wanted to run my helloWorldPromise and then helloWorld tasks, I’d make the following changes:

const { series } = require('gulp');

exports.promiseFirst = series(helloWorldPromise, helloWorld);

Running the task with the command gulp promiseFirst yields:

[14:16:06] Using gulpfile ~/dev/hello-gulp/gulpfile.js
[14:16:06] Starting 'promiseFirst'...
[14:16:06] Starting 'helloWorldPromise'...
hello world promise
[14:16:06] Finished 'helloWorldPromise' after 1.68 ms
[14:16:06] Starting 'helloWorld'...
hello world
[14:16:06] Finished 'helloWorld' after 387 μs
[14:16:06] Finished 'promiseFirst' after 7.23 ms

Gulp can do more than print things to the command line. You can process files with pipe, gulp.src() and gulp.dest().

gulp.src() produces a stream of Vinyl objects. Each Vinyl object contains metadata for a file. gulp.dest() takes a stream and saves it to a file. The pipe() , in between gulp.src() and gulp.dest(), turns the Vinyl file metadata object into a byte stream you can manipulate.

const { src, dest } = require('gulp');

function copyTextFiles() {
    return src('*.txt')
        .pipe(dest('output/'));
}

The previous function is very simple. We can write a new Gulp task that takes the contents of each text file and reverses the text inside. I’m going to import a new Nodejs module, gulp-tap, which enables me to “tap” into a pipeline.

npm i gulp-tap

Using gulp-tap:

const { src, dest } = require('gulp');
const tap = require('gulp-tap');

function reverseTextFiles() {
    return src('*.txt')
        .pipe(tap(file => console.log(file)))
        .pipe(dest('output/'));
}

The output is:

[11:32:06] Using gulpfile ~/source/hello-gulp/gulpfile.js
[11:32:06] Starting 'printTextFiles'...
<File "hello.txt" <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64 0a>>
[11:32:06] Finished 'printTextFiles' after 46 ms

Tap allows access to the underlying stream. Once I can do that, reversing the file contents is easy. Let’s write our custom reversing function and incorporate it into our pipeline using tap.

function reverseTextFiles() {
    return src('*.txt')
        .pipe(tap(file => reverseText(file)))
        .pipe(dest('output/'));
}

function reverseText(file) {
    let reversed = file.contents.toString()
        .split("")
        .reverse()
        .join("");
    file.contents = Buffer.from(reversed);
}

exports.reverse = reverseTextFiles;

Running this script, gulp reverse, results in one or more .txt files with its contents reversed.

Here is my final gulpfile.

const { series, src, dest } = require('gulp');
const tap = require('gulp-tap');

function helloWorld(cb) {
    console.log('hello world');
    cb();
}

function helloWorldPromise() {
    return new Promise((resolve, reject) => {
        console.log('hello world promise');
        resolve();
    });
}

function reverseTextFiles() {
    return src('*.txt')
        .pipe(tap(file => reverseText(file)))
        .pipe(dest('output/'));
}

function reverseText(file) {
    let reversed = file.contents.toString()
        .split("")
        .reverse()
        .join("");
    file.contents = Buffer.from(reversed);
}

exports.default = helloWorld;
exports.helloWorld = helloWorld;
exports.helloWorldPromise = helloWorldPromise;
exports.promiseFirst = series(helloWorldPromise, helloWorld);
exports.reverse = reverseTextFiles;

What’s Next

Using gulp-tap makes it really easy to introduce custom functionality into a gulp pipeline. I’m going to leverage this package to generate a feature image, using Puppeteer, for all of my blog posts in Part 2 of this series. Watch out for it in the coming weeks.

🧇