Moving from gulp 3 to 4

A white number four on a blue background

Introduction

We typically create boilerplates for project types that come up a lot (e.g. WordPress plugins, themes, etc...). They get updated every so often with small adjustments. But, I'd been running into situations where the code I was writing wasn't supporting legacy browsers as well as I would have liked (I'm looking at you IE11....).

As part of the boilerplates, we typically use gulp js. Gulp is a javascript task runner. That means it lets you automate common tasks like

  • moving and renaming files
  • turning sass files into CSS
  • merging files together
  • turning newer versions of javascript into more supported versions
  • minifying files

It does most of these things through plugins to the base gulp package.

The version our boilerplate was using was version 3, but version 4 of gulp's been out for a while and is well supported. So, it was time for an upgrade.

Goals

I had a few goals that I wanted to address when making this update. Some are carry-overs from the previous boilerplate while others are new based on problems I'd faced. The main changes were

  • Making sure our browser support was more consistently defined
  • Making sure our css files were autoprefixed better and took CSS grid into account.
  • Making sure our js files could be polyfilled more consistently and (again) with better browser support

In the end, I ended up with a combination of gulp and webpack to make it all happen though.

 

Dependencies

Gulp depends on an ecosystem of plugins and utilities to actually accomplish anything useful. Which isn't to say that gulp's not great (because it is). It's just that it requires installing and managing a fair number of things to do the tasks you want. Here is a list of the dependencies I installed to support version 4.

These get installed with javascript's node package manager (npm) like this npm install -D [devDependencies] or npm install -D [dependencies]

 

Browser Support

To make sure that browser support is consistent across a variety of tools, I included a .browserslistrc file. This way, gulp, webpack, autoprefixer, and a variety of other tools will know which browsers I want to support or exclude. If these requirements ever change, the file can be updated easily.

It's a very small file that basically says we're going to try to support all browsers except Internet Explorer 9 or less.

Browser List

Babel Environment

Next comes a .babelrc file. This file will reference the .browserslist file via the @babel/preset-env babel plugin. So we don't have to worry about writing ES6 with polyfills and having it come out un-useable by IE 11 (for instance).

The properties here will tell babel and webpack that we want to

  • check for browser compatibility
  • leave debugging on (since no one will see it except us anyway)
  • only load polyfills if they're needed (via the useBuiltIns and corejs properties)

Setting up gulp tasks

Now that browser support is in place, we can move on to writing gulp tasks in our gulpfile. You can do a lot of things with gulp and I certainly am not going to cover everything. For now, we're going to look at tasks to

  • catch errors and log them - onError()
  • clean a directory - clean()
  • transform sass files into css files - styles()
  • turn es6 into es5 - scripts()
  • watch files in a directory - watch()
  • manage files for a one-off build - default()

Here's the full gulpfile. It's divided into several parts each with an explanation.

Configuration

Because gulp runs in node, we can require javascript packages and files to use within the gulpfile. That's what the first section does.

  • Gulp is obviously the main package we need. Without it, nothing else is going to happen.
  • Browsersync helps with automatically reloading the page for the local environment when changes are made to the files we want to watch.
  • Next, webpack-stream helps integrate our webpack configuration with gulp. It lets us send files from gulp through webpack and then back to gulp.
  • The del package helps remove files and directories as part of any cleanup tasks we want to do. It should not be confused with the frozen treat found in Rhode Island.
  • The pluginOpts object lets us configure the gulp-load-plugins plugin. This plugin is a kind of meta-plugin. Mostly it gets used so that we don't have to declare and keep track of every single other gulp plugin we need in the file.
  • In order to use webpack to the best of its capabilities, we split it into several files. I'll get into that later. For now, it's enough to know that the right one is chosen based on the environment. When working locally, we want to use a development configuration. When the code is built and sent to production, we want to make sure it at least gets minified, so we want a production configuration.
  • The paths object is just a convenient way of keeping track of which files we want to reference.

Utilities

There are two utility functions that are helpful in this file: onError and clean.

The onError function gets used as part of a configuration object for the gulp-plumber plugin. Gulp-plumber helps make sure that when there are errors thrown by tasks, we don't have to restart the node environment running gulp. The function receives the error and displays it in whatever terminal window is being used to see the running tasks.

The clean function calls the del function and removes the contents of the directory where assets built by gulp are sent. This is useful in the default gulp task. This way, we know we'll start with a blank slate. In the watch task, it's used to make sure we're changing hashed asset names as needed. Any files in the build directory after this function is called are guaranteed to be new. Otherwise, there's a risk that old files may be in the build directory by mistake.

Task Functions

In this gulpfile, there are two task functions: styles and scripts. When they're run, these functions take a group of files and pipe them through a variety of transformations.

The styles function has the following steps

  • Start with the source files passed to gulp.src
  • Capture and log any errors produced by the files (like when you forget to add a semi-colon...)
  • Start creating a sourcemap for the finished file. This way you know where among the many sass files you have a particular line of code comes from.
  • The sass plugin takes the sass files and turns them into a single CSS file. In this case, it also compresses the file. Small file sizes are good!
  • That CSS file is sent to autoprefixer to help with browser compatibility. The autoplace CSS grid option helps with Internet Explorer 10 and 11 compatibility.
  • The sourcemaps and finished CSS files are then written to the correct destination directory.
  • Last, browser-sync tries to update the browser once the new files are available.

In code, that looks like this:

The scripts function uses a similar series of steps. However, whereas the styles function stays "within" gulp, javascript files are sent for processing to webpack and then returned via webpack-stream. The files are then written to their destination directory.

Gulp Tasks

This is all well and good, but you can't really do anything with these functions until they're part of a gulp task. A gulp task will be called from the terminal (or a build process) with gulp <task-name>. So, when we want to watch the sass and js files for changes and run them through the styles or scripts functions to do things, we want to type in gulp watch and have node start the correct series of functions.

Gulp 4 introduces a different way of ordering the how functions are processed using gulp.series and gulp.parallel. The series method, "Combines task functions and/or composed operations into larger operations that will be executed one after another, in sequential order." While the parallel method, "Combines task functions and/or composed operations into larger operations that will be executed simultaneously." (gulp series documentation, gulp parallel documentation)

What does this mean? A version 3 gulp watch task might look something like this...

The same gulp task in version 4 looks like this...

In both cases, the result will be basically the same. When the command gulp watch is run, the sass and js files will be updated immediately, the files will be watched for changes, and any changes will trigger updates via the styles and scripts functions.

By using the parallel method, we can ensure that it doesn't matter if we're working on javascript or sass files. The updates will take place regardless of the order they happen in.

If however, we care about the order of events, we can use the series method. For example in the default gulp task, we want to make sure that the build directory is cleaned before anything else happens. To do that, we can write this way...

Note that it's completely fine to nest a call to the parallel method inside the series method.

Webpack

The last thing we need to address is how to configure webpack so that we have good browser support and encourage development practices. To do that, we want to split the configuration into three files as the webpack documentation suggests.

  • webpack.common.js
  • webpack.dev.js
  • webpack.prod.js

The common file will be used by the dev and prod files. It will be used to

  • read the main index.js file
  • direct the output of any transformations to the build directory
  • test javascript files and apply the babel-loader to them which will reference the .babelrc file we set up earlier. This way we can write ES6, use imports, etc... and ensure that we have a single js file with good browser support at the end.

The webpack.dev.js file sets the mode to develop and enables webpack sourcemaps as an option. These could be used instead of gulp sourcemaps if we wanted to. There's no conflict as far as I can tell between using both. This file is combined with the common file using webpack-merge.

Lastly, the webpack.prod.js file sets the mode to production. This way, we can take advantage of minification and send smaller files to our production servers.

Conclusion

Moving from gulp 3 to 4 was a great learning experience for me. It was an opportunity for us to improve our browser support and tooling moving forward. It was also a way for me to (re-)learn some techniques as well.

Posted by Adam Berkowitz