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.
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.
Now we need to install Gulp 4 locally. Since it’s not officially released, we’ll need to get it straight from Github:
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.
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.
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:
But what about the 3-parameter signature? How do we specify a dependency task? You will do so by using the new
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:
It would be changed to
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.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
would be changed to
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.
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
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:
Note that the
scripts tasks both depend on the
clean task. When you run the default task, it’ll try to run both
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
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.
This is because
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.
This of course means that you can’t just call the
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.
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:
You can provide a callback parameter to your task’s function and then call it when the task is complete:
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.
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:
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.
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.
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!
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
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:
As seen in the
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.
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:
There are a few things to note here:
- Thanks to hoisting, the functions can be defined below the definition of the
defaulttask, 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.
cleanare now “private” tasks, so they cannot be run using the Gulp command line.
- No more anonymous functions.
- 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.
- The “tasks” can be split into multiple files and easily imported into a single file that uses
gulp.taskto define the runnable tasks.
- 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:
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
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.
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:
Or to make it shorter and not add another variable:
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:
If you run
gulp --tasks on this you’ll see this:
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.