The Complete-Ish Guide to Upgrading to Gulp 4

Gulp 4 has been in the works for far too long, but it’s practically inevitable that it’ll be released… some day. I’m here to help you out for when that fateful day arrives by showing you the differences between Gulp 3.x and Gulp 4 and how you can make the migration to the new version relatively painless.

Just a note to prevent potential confusion, and to calm those who are sticklers for correct usage of terms (I’m one of those people), I use the word “compose” throughout this article; I’m using it in the general sense, not in the functional programming sense. “Compose” sounds more elegant than “combine” and its meaning is slightly closer to the idea I’m trying to convey.

Installation

Before you can start using the latest version of Gulp, you’ll need to get rid of your current version of Gulp. Normally, you can just update the version number in you package.json file, but there are a few things preventing you from upgrading quite so easily. The most obvious reason is that you probably have Gulp installed locally in the project and globally on your machine (If you’re one of the people who follows the practice of using npm scripts to access the locally installed version of CLI’s, great! But that still won’t help you much here). So, first make sure you uninstall Gulp locally, and if you have it installed globally, uninstall it there as well.

1
2
npm uninstall gulp --save-dev
npm uninstall gulp -g

Now we need to install Gulp 4 locally. Since it’s not officially released, we’ll need to get it straight from Github:

1
npm install gulpjs/gulp.git#4.0  --save-dev

Once it’s released, you can just use the normal npm install gulp --save-dev. Also, when it’s finally released, we’ll need to update our projects to remove the Github version and install the npm version. For right now, there’s another thing we need to install: the CLI tool. With version 4, Gulp is separating the CLI tool from the actual Gulp code, much like Grunt does today. This separate CLI tool will actually work for both Gulp 3 and Gulp 4 right now.

1
npm install gulp-cli --save-dev

If you don’t use npm scripts, then you’ll need to use -g instead of --save-dev to install it globally. Now you can use the same gulp command that you had previously, but you’re going to see errors because you’ll need to update your Gulpfile.js to be compatible with the new version of Gulp.

Tasks Refactored

If you’re doing simple tasks that have no dependencies whatsoever, you’re in luck! You don’t have to make any changes! Sadly, real people have to make changes. The big change here is that Gulp now only supports the 2-parameter version of gulp.task. When you use 2 parameters, it takes a string as the name of the task, and a function to run for that task. e.g. the following task would remain the same between version 3.x and 4:

1
gulp.task('clean', function() {...})

But what about the 3-parameter signature? How do we specify a dependency task? You will do so by using the new gulp.series and gulp.parallel functions. Each of these functions will take a list of functions or task name strings and return anothe function. In the case of gulp.series, it’ll return a function that runs each of the given tasks/functions sequentially in the order they were provided whereas gulp.parallel will return a function that runs each of the given tasks/function in parallel. Finally, Gulp has given us the ability to choose between sequential and parallel execution of tasks without the need of another dependency (traditionally run-sequence) or a bunch of crazy task dependency arrangement.

So, if you have this task before:

1
2
3
gulp.task('styles', ['clean'], function() {
...
});

It would be changed to

1
2
3
gulp.task('styles', gulp.series('clean', function() {
...
}));

When making the swap, don’t forget that your task’s main function is now inside the gulp.series call, so you’ll need the extra parenthesis at the end. This can be easy to miss.

Note that since gulp.series and gulp.parallel return functions, they can be nested, and you’ll probably need to nest them often if your tasks tend to have multiple dependency tasks, e.g. this common pattern

1
2
3
gulp.task('default', ['scripts', 'styles'], function() {
...
});

would be changed to

1
2
3
gulp.task('default', gulp.series(gulp.parallel('scripts', 'styles'), function() {
...
}));

Sadly, this is often a bit messier to read than the old ways, but it’s a small price to pay for greater flexibility and control. You can also write some helper/alias function to make this more terse if that’s your preference, but I won’t get into that.

Dependency Gotchas

In Gulp 3, if you specified several tasks that had the same dependency task, and each of these tasks was run, Gulp would recognize that all of these tasks depended on the same task and only run that depended-upon task once. Since we’re no longer specifying “dependencies”, rather we’re combining several functions together using series or parallel, Gulp can’t determine which tasks will be run multiple times when it should only be run once, so we’ll need to change the way we work with dependencies.

That’s a lot of abstract jargon being thrown around, so how about an example to clarify things? This example is adapted from an article on the Front-End Technology Talk about Gulp 4’s new task execution system, and they spend most of that article on this topic, so if I’m not clear enough, that article should bring some clarity.

Take a look at this example from Gulp 3:

1
2
3
4
5
6
7
8
9
// Per default, start scripts and styles
gulp.task('default', ['scripts', 'styles'], function() {...});

// Both scripts and styles call clean
gulp.task('styles', ['clean'], function() {...});
gulp.task('scripts', ['clean'], function() {...});

// Clean wipes out the build directory
gulp.task('clean', function() {...});

Note that the styles and scripts tasks both depend on the clean task. When you run the default task, it’ll try to run both styles and scripts, see that they have dependencies, try to run each of the dependencies first, realize that both tasks depend on the clean task, and ensure that the clean task is run only once before coming back to the styles and scripts tasks. That’s a very helpful feature! Sadly, it could not be ported to the new way of doing things. If you just naively make the simple changes to Gulp 4 like I do in the following example, clean will be run twice.

1
2
3
4
5
gulp.task('clean', function() {...});
gulp.task('styles', gulp.series('clean', function() {...}));
gulp.task('scripts', gulp.series('clean', function() {...}));

gulp.task('default', gulp.parallel('scripts', 'styles'));

This is because parallel and series do not specify dependencies; they simply combine multiple functions into a single function. So we’ll need to pull dependencies out of each task, and specify the dependencies as a series in the larger “parent” task:

Important note: You cannot define default before you define any of the smaller tasks it composes. When you call gulp.series("taskName"), the task with the name "taskName" needs to be defined already. This is why we moved default to the bottom for Gulp 4 whereas it could be anywhere in Gulp 3.

1
2
3
4
5
6
7
// The tasks don't have any dependencies anymore
gulp.task('styles', function() {...});
gulp.task('scripts', function() {...});
gulp.task('clean', function() {...});

// Per default, start scripts and styles
gulp.task('default', gulp.series('clean', gulp.parallel('scripts', 'styles')));

This of course means that you can’t just call the styles or scripts task independently while getting the prerequisite clean done, however, the way this was set up, clean would clean out the scripts and styles areas, so I’m not sure you would have been calling them independently anyway.

Asynchronous Task Support

In Gulp 3, if the code you ran inside a task function was synchronous, there was nothing special that needed to be done. That’s changed in Gulp 4: now you need to use the done callback (which I’ll get to shortly). Also, for asynchronous tasks, you had 3 options for making sure Gulp was able to recognize when your task finished, which were:

1) Callback

You can provide a callback parameter to your task’s function and then call it when the task is complete:

1
2
3
4
5
var del = require('del');

gulp.task('clean', function(done) {
del(['.build/'], done);
});

2) Return a Stream

You can also return a stream, usually made via gulp.src or even by using the vinyl-source-stream package directly. This will likely be the most common way of doing things.

1
2
3
4
5
gulp.task('somename', function() {
return gulp.src('client/**/*.js')
.pipe(minify())
.pipe(gulp.dest('build'));
});

3) Return a Promise

Promises have been growing in prominence and are now even being implemented directly into Node, so this is a very helpful option. Just return the promise and Gulp will know when it’s finished:

1
2
3
4
5
var promisedDel = require('promised-del');

gulp.task('clean', function() {
return promisedDel(['.build/']);
});

New Asynchronous Task Support

Now, thanks to Gulp’s use of the async-done package and its latest updates we have support for even more ways of signalling a finished asynchronous task.

4) Return a Child Process

You now spawn child processes and just return them! You can essentially move your npm scripts into Gulp with this if you’re not really a fan of loading up your package.json file with a million commands or using a lot of Gulp plugins that can get out of date with the packages they’re wrapping. Might look a bit like an anti-pattern, though, and there are other ways to do this as well.

1
2
3
4
5
var spawn = require('child_process').spawn;

gulp.task('clean', function() {
return spawn('rm', ['-rf', path.join(__dirname, 'build')]);
});

5) Return a RxJS observable

I have never used RxJS, and it seems kinda niche, but for those who love this library to death, you may be very pleased to just be able to return an observable!

1
2
3
4
5
var Observable = require('rx').Observable;

gulp.task('sometask', function() {
return Observable.return(42);
});

Watching

The API for watching the file system and reacting to changes has had a bit of a makeover as well. Previously, after passing a glob pattern and optionally passing some options in, you were able to either pass in an array of tasks or a callback function that got some event data passed to it. Now, since tasks are specified via series or parallel which simply return a function, there’s no way to distinguish tasks from a callback, so they’ve removed the signature with a callback. Instead, like before, gulp.watch will return a “watcher” object that you can assign listeners to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// OLD VERSION
gulp.watch('js/**/*.js', function(event) {
console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});

// WILL CHANGE TO:
var watcher = gulp.watch('js/**/*.js' /* You can also pass options and/or a task function here */);
watcher.on('all', function(event, path, stats) {
console.log('File ' + path + ' was ' + event + ', running tasks...');
});

// OR LISTEN TO INDIVIDUAL EVENT TYPES
watcher.on('change', function(path, stats) {
console.log('File ' + path + ' was changed, running tasks...');
});

watcher.on('add', function(path) {
console.log('File ' + path + ' was added, running tasks...');
});

watcher.on('unlink', function(path) {
console.log('File ' + path + ' was removed, running tasks...');
});

As seen in the any and change handlers, you may also receive a file stats object. The stats only show up with their available (not sure when they would or would not be), but you can set the alwaysStat option to true if you always want it to show up. Gulp is using chokidar under the hood so you can look at their documentation for greater details, though it doesn’t accept the third argument for a function to run on every event.

Using Plain Functions

Since every task is essentially just a function now, with no dependencies or anything special, other than the fact that they need a special task runner to determine when asynchronous tasks finish, we can move away from using gulp.task for everything and start embracing independent functions rather than functions merely as callbacks being passed to gulp.task. For example, I would change the end result of the example we came to in the “Dependency Gotchas” section above from this:

1
2
3
4
5
gulp.task('styles', function() {...});
gulp.task('scripts', function() {...});
gulp.task('clean', function() {...});

gulp.task('default', gulp.series('clean', gulp.parallel('scripts', 'styles')));

to this:

1
2
3
4
5
6
7
// Just use function names with `series` and `parallel` to compose a task
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles)));

// Change individual tasks to plain functions
function styles() {...}
function scripts() {...}
function clean() {...}

There are a few things to note here:

  1. Thanks to hoisting, the functions can be defined below the definition of the default task, unlike before where the tasks that it composes together need to be defined beforehand. This allows you to define the actual runnable tasks at the top for people to find more easily, rather than defining the pieces of the tasks first and hiding the runnable task in the mess at the bottom.
  2. styles, scripts, and clean are now “private” tasks, so they cannot be run using the Gulp command line.
  3. No more anonymous functions.
  4. No more wrapping “task” names in quotes, which also means that you’re using an identifier that your code editor/IDE can recognize is not defined if you mispell it, instead of needing to wait until you run Gulp to get the error.
  5. The “tasks” can be split into multiple files and easily imported into a single file that uses gulp.task to define the runnable tasks.
  6. Each of these tasks is independently testable (if you feel the need) without needing Gulp at all.

Of course, #2 can be rectified if you want them to be runnable by the Gulp command line:

1
gulp.task(styles);

This will make the new task called “styles” that you can run from the command line. Note that I never specified a task name here. gulp.task is smart enough to grab the name right off of the function. This won’t work with an anonymous function, of course: Gulp throws an error if you try to assign an anonymous function as a task without providing a name.

If you want to give the function a custom name, you can use the function’s displayName property.

1
2
3
function styles(){...}
styles.displayName = "pseudoStyles";
gulp.task(styles);

Now the task’s name will be “pseudoStyles” instead of “styles”. You can also use the description property to give details about what the task does. You can view these details with the gulp --tasks command.

1
2
3
4
function styles(){...}
styles.displayName = "pseudoStyles";
styles.description = "Does something with the stylesheets."
gulp.task(styles);
1
2
3
$ gulp --tasks
[12:00:00] Tasks for ~/project/gulpfile.js
[12:00:00] └── pseudoStyles Does something with the stylesheets.

You can even add descriptions to other tasks that have been registered like default. You’ll first have to use gulp.task('taskName') to retrieve the task that was already assigned, then give it a description:

1
2
3
4
5
6
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles)));

// Use gulp.task to retrieve the task
var defaultTask = gulp.task('default');
// give it a description
defaultTask.description = "Does Default Stuff";

Or to make it shorter and not add another variable:

1
2
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles)));
gulp.task('default').description = "Does Default Stuff";

These descriptions can be very helpful to people who aren’t familiar with your project, so I recommend using them wherever applicable: it can be more useful and accessible than normal comments sometimes. In the end, this is the pattern I recommend as the best practice for Gulp 4:

1
2
3
4
5
6
gulp.task('default', gulp.series(clean, gulp.parallel(scripts, styles)));
gulp.task('default').description = "This is the default task and it does certain things";

function styles() {...}
function scripts() {...}
function clean() {...}

If you run gulp --tasks on this you’ll see this:

1
2
3
4
5
6
7
8
$ gulp --tasks
[12:00:00] Tasks for ~\localhost\gulp4test\gulpfile.js
[12:00:00] └─┬ default This is the default task and it does certain things
[12:00:00] └─┬ <series>
[12:00:00] ├── clean
[12:00:00] └─┬ <parallel>
[12:00:00] ├── scripts
[12:00:00] └── styles

Not only does your description do the talking, the names of the functions that make up the task will give plenty of insight as well. If you disagree that the above pattern is the way it should be done, fine with me. That should really be a discussion you have with your team.

In any case, I see some helpful improvements coming with Gulp, but it’s different enough to cause some potential headaches during migration. I pray this guide is enough for you to migrate over to Gulp 4 when the time comes (some days…). God bless and happy coding.

Author: Joe Zimmerman

Author: Joe Zimmerman Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others though his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word.