Getting Started with Gulp.js
Developers spend precious little time coding. Even if we ignore irritating meetings, much of the job involves basic tasks that can sap your working day:
- generating HTML from templates and content files
- compressing new and modified images
- compiling Sass to CSS code
- removing
console
anddebugger
statements from scripts - transpiling ES6 to cross-browser-compatible ES5 code
- code linting and validation
- concatenating and minifying CSS and JavaScript files
- deploying files to development, staging and production servers
Tasks must be repeated every time you make a change. You may start with good intentions, but the most infallible developer will forget to compress an image or two. Over time, pre-production tasks become increasingly arduous and time-consuming; you’ll dread the inevitable content and template changes. It’s mind-numbing, repetitive work. Would it be better to spend your time on more profitable jobs?
If so, you need a task runner or build process.
That Sounds Scarily Complicated!
Creating a build process will take time. It’s more complex than performing each task manually but, over the long-term, you’ll save hours of effort, reduce human error and save your sanity. Adopt a pragmatic approach:
- Automate the most frustrating tasks first.
- Try not to over-complicate your build process; an hour or two is more than enough for the initial set-up.
- Choose task runner software and stick with it for a while. Don’t switch to another option on a whim.
Some of the tools and concepts may be new to you, but take a deep breath and concentrate on one thing at a time.
Task Runners: the Options
Build tools such as GNU Make have been available for decades, but web-specific task runners are a relatively recent phenomenon. The first to achieve critical mass was Grunt —a Node.js task runner that used plugins configured (originally) by a JSON configuration file. Grunt was hugely successful but suffered with speed and complex customization problems. Many issues were addressed in later editions, but Gulp had already arrived and offered a number of improvements:
- Features such as file watching were built-in.
- Gulp plugins were (mostly) simple and designed to do a single job.
- Gulp used JavaScript configuration code which was less verbose, easier to read, simpler to modify, and provided better flexibility.
- Gulp was faster because it used Node.js streams to pass data through a series of piped plugins. Files were only written at the end of each task.
Since then, several competing alternatives have arrived:
- npm is a option for simpler projects
- webpack and Parcel have increased in popularity. These are module bundlers that understand JavaScript syntax and can perform simple tasks with little configuration.
However, Gulp still provides the flexibility and efficiency to attract a third of web developers. If you can write JavaScript, Gulp allows general-purpose tasks to do anything.
Gulp requires Node.js. JavaScript knowledge is beneficial but developers from all web programming faiths will find it useful.
Gulp Versions
Gulp.js 3.9.1 has been the default version many years and, while Gulp 4 was available, it had to be explicitly installed with npm install gulp@next
. This was necessary because, although plugins remain compatible, Gulp 4 used a new configuration syntax.
On December 10, 2018, Gulp.js 4.0 was announced as the default and published to npm. Anyone using npm install gulp
on a new project will receive version 4. That will be used throughout this guide.
Step 1: Install Node.js
Node.js can be downloaded for Windows, macOS and Linux from nodejs.org/download/. There are various options for installing from binaries, package managers and docker images; full instructions are available.
Plugins and Windows
Node.js and Gulp run on Windows but some plugins may not work correctly if they depend on native Linux binaries. One option for Windows 10 users is the Windows Subsystem for Linux; this could solve issues but may introduce alternative problems.
Once installed, open a command prompt and enter the following to reveal the version number:
node -v
You’re about to make heavy use of npm
—the Node.js package manager that’s used to install modules. Examine its version number:
npm -v
Permissions on Linux
Node.js modules can be installed globally so they are available throughout your system. However, most users will not have permission to write to the global directories unless npm
commands are prefixed with sudo
. There are a number of options to fix npm permissions and tools such as nvm can help but you can also change the default directory, e.g. on Ubuntu/Debian-based platforms:
cd ~ mkdir .node_modules_global npm config set prefix=$HOME/.node_modules_global npm install npm -g
Then add the following line to the end of ~/.bashrc
:
export PATH="$HOME/.node_modules_global/bin:$PATH"
Then update with this:
source ~/.bashrc
Step 2: Install Gulp Globally
Install the Gulp command-line interface globally so the gulp
command can be run from any project folder:
npm install gulp-cli -g
Verify Gulp has installed with the following:
gulp -v
Step 3: Configure Your Project
Node.js Projects
You can skip this step if you already have a package.json
configuration file.
Let’s assume you have a new or pre-existing project in the folder project1
. Navigate to this folder and initialize it with npm:
cd project1npm init
You’ll be asked a series of questions: enter a value or hit Return to accept defaults. A package.json
file that stores your npm
configuration settings will be created on completion.
Node and Git
Node.js installs modules to a node_modules
folder. You should add this to your .gitignore
file to ensure they’re not committed to your repository. When deploying the project to another system, you can run npm install
to restore them.
For the remainder of this guide, we’ll presume your project folder contains the subfolders itemized below.
src
Folder: Pre-processed Source Files
This contains further subfolders:
html
—HTML source files and templatesimages
—the original uncompressed imagesjs
—multiple pre-processed script filesscss
—multiple pre-processed Sass.scss
files
build
Folder: Compiled/processed Files
Gulp will create files and create subfolders as necessary:
html
—compiled static HTML filesimages
—compressed imagesjs
—a single concatenated and minified JavaScript filecss
—a single compiled and minified CSS file
Your project will almost certainly be different, but this structure is used for the examples below.
Recreating the Source Folder on a Unix-based System
If you’re on a Unix-based system and you just want to follow along with this guide, you can recreate the source folder structure with the following command:
mkdir -p src/{html,images,js,scss}
Step 4: Install Gulp Locally
You can now install Gulp in your project folder using this command:
npm install gulp --save-dev
This installs Gulp as a development dependency and the "devDependencies"
section of package.json
is updated accordingly. We’ll presume Gulp and all plugins are development dependencies for the remainder of this guide.
Alternative Deployment Options
Development dependencies are not installed when the NODE_ENV
environment variable is set to production
on your operating system. You would normally do this on your live server with the macOS/Linux command:
export NODE_ENV=production
Or on Windows:
set NODE_ENV=production
This guide presumes your assets will be compiled to the build
folder and committed to your Git repository or uploaded directly to the server. However, it may be preferable to build assets on the live server if you want to change the way they’re created—for example, HTML, CSS and JavaScript files being minified on production but not development environments. In that case, remove the --save-dev
switch when installing Gulp and its plugins. For example:
npm install gulp
This sets Gulp as an application dependency in the "dependencies"
section of package.json
. It will be installed when you enter npm install
and can be run wherever the project is deployed. You can remove the build
folder from your repository, since the files can be created on any platform when required.
Step 5: Create a Gulp Configuration File
Create a new gulpfile.js
configuration file in the root of your project folder. Add some basic code to get started:
// Gulp.js configuration
const // modules gulp = require('gulp'),
// development mode? devBuild = (process.env.NODE_ENV !== 'production'),
// folders src = 'src/', build = 'build/' ;
This references the Gulp module, sets a devBuild
variable to true
when running in development (or non-production mode) and defines the source and build folder locations.
ES6 and Node
ES6 code is provided in this guide. This will work in Node.js from version 6.0 and above.
gulpfile.js
won’t do anything yet, because you need to follow the next step described below.
Step 6: Create Gulp Tasks
On its own, Gulp does nothing. You must:
- install Gulp plugins, and
- write tasks that utilize those plugins to do something useful
It’s possible to write your own plugins but, since almost 3,000 are available, it’s unlikely you’ll ever need to. You can search using Gulp’s own directory at gulpjs.com/plugins/, on npmjs.com, or by harnessing the mighty power of Google and searching for “gulp something ”.
Most tasks will use:
gulp.src(folder)
to create stream from files in a source folder, andgulp.dest(folder)
to output the stream as files to a destination build folder
Any number of plugin methods can be called with .pipe(plugin)
between the .src
and .dest
.
Image Task
This is best demonstrated with an example, so let’s create a basic task that compresses images and copies them to the appropriate build
folder. Since this process could take time, we’ll only compress new and modified files. Two plugins can help us: gulp-newer and gulp-imagemin. Install them from the command-line:
npm install gulp-newer gulp-imagemin --save-dev
We can now reference both modules at the top of gulpfile.js
:
// Gulp.js configuration
const // modules gulp = require('gulp'), newer = require('gulp-newer'), imagemin = require('gulp-imagemin'),
We can now define an image processing function at the end of gulpfile.js
:
// image processingfunction images() {
const out = build + 'images/';
return gulp.src(src + 'images/**/*') .pipe(newer(out)) .pipe(imagemin({ optimizationLevel: 5 })) .pipe(gulp.dest(out));
});exports.images = images;
All tasks are syntactically similar. This code does the following:
- It creates a new task function named
images
. - It defines an
out
folder where build files will be located. - It reads a stream of files from the
src/images/
source folder. The**/*
ensures that images in subfolders are also processed. - It pipes all files to the
gulp-newer
module. Source files that are newer than corresponding destination files are passed through. Everything else is removed. - It pipes remaining new/changed files through
gulp-imagemin
, which sets an optionaloptimizationLevel
argument. - It outputs compressed images to the Gulp
dest/images/
folder. - It exports a public
images
task which calls theimages
function.
Save gulpfile.js
and place a few images in your project’s src/images/
folder before running the task from the command line:
gulp images
All images are compressed accordingly and you’ll see output such as this:
Using file gulpfile.jsRunning 'imagemin'...Finished 'imagemin' in 5.71 msgulp-imagemin: image1.png (saved 48.7 kB)gulp-imagemin: image2.jpg (saved 36.2 kB)gulp-imagemin: image3.svg (saved 12.8 kB)
Try running gulp images
again; no files are processed because no image files have been changed.
HTML Task
We can now create a similar task that copies files from the source HTML folder. We can safely minify our HTML code to remove unnecessary whitespace and attributes using the gulp-htmlclean plugin.
The gulp-noop plugin will also be installed. This performs no operation, which can be useful for simple development/production processing decisions:
npm install gulp-htmlclean gulp-noop --save-dev
These modules are loaded at the top of gulpfile.js
:
const // modules gulp = require('gulp'), noop = require('gulp-noop'), newer = require('gulp-newer'), imagemin = require('gulp-imagemin'), htmlclean = require('gulp-htmlclean'),
We can now export an html
function at the end of gulpfile.js
:
// HTML processingfunction html() { const out = build + 'html/';
return gulp.src(src + 'html/**/*') .pipe(newer(out)); .pipe(devBuild ? noop() : htmlclean()) .pipe(gulp.dest(out));}exports.html = gulp.series(images, html);
This reuses gulp-newer
and introduces a couple of concepts:
- We only pipe the HTML through
gulp-htmlclean
ifNODE_ENV
is set toproduction
. Therefore, the HTML remains uncompressed during development, which may be useful for debugging. - The exported
html
task usesgulp.series()
to combine tasks that are executed one after the other. In this case, theimages()
function is run prior tohtml()
, which ensures HTML files can reference images.
Save gulpfile.js
and run gulp html
from the command line. Both the html
and images
tasks will run.
JavaScript Task
Too easy for you? Let’s process JavaScript files by building a basic module bundler. It will:
- Ensure dependencies are loaded first using the gulp-deporder plugin. This analyses comments at the top of each script to ensure correct ordering—such as
// requires: defaults.js lib.js
. - Concatenate all script files into a single
main.js
file using gulp-concat. - Remove all
console
anddebugging
statements with gulp-strip-debug when running in production mode. - Minimize code with the ES6-compatible gulp-terser.
- Append a source map when running in development mode with gulp-sourcemaps.
Install the plugin modules:
npm install gulp-deporder gulp-concat gulp-strip-debug gulp-terser gulp-sourcemaps --save-dev
Then load them them at the top of gulpfile.js
:
const ... concat = require('gulp-concat'), deporder = require('gulp-deporder'), terser = require('gulp-terser'), stripdebug = devBuild ? null : require('gulp-strip-debug'), sourcemaps = devBuild ? require('gulp-sourcemaps') : null,
Only in Development
The gulp-strip-debug
and gulp-sourcemaps
modules are only loaded in development mode for efficiency.
Next, export a new js
task function:
// JavaScript processingfunction js() {
return gulp.src(src + 'js/**/*') .pipe(sourcemaps ? sourcemaps.init() : noop()) .pipe(deporder()) .pipe(concat('main.js')) .pipe(stripdebug ? stripdebug() : noop()) .pipe(terser()) .pipe(sourcemaps ? sourcemaps.write() : noop()) .pipe(gulp.dest(build + 'js/'));
}exports.js = js;
The function is much the same as other tasks, but the sourcemap.init()
function is called prior to code transformations and sourcemaps.write()
is called after they’ve been completed.
Save, add a few JavaScript files to the src/js/
folder, then run gulp js
to watch the magic happen!
CSS Task
Finally, let’s create a CSS task that compiles Sass .scss
files to a single .css
file using gulp-sass. This is a Gulp plugin for node-sass, which binds to the super-fast LibSass C/C++ port of the Sass engine. We’ll presume your primary Sass file scss/main.scss
is responsible for loading all partials.
Our task will also utilize the fabulous PostCSS via the gulp-postcss plugin. PostCSS requires its own set of plugins, and we’ll install these ones:
- postcss-assets to manage assets. This allows us to use properties such as
background: resolve('image.png');
to resolve file paths orbackground: inline('image.png');
to inline data-encoded images. - autoprefixer to automatically add vendor prefixes to CSS properties.
- css-mqpacker to pack multiple references to the same CSS media query into a single rule.
- cssnano to minify the CSS code when running in production mode.
Most of these plugins accept parameters; refer to their documentation for more information.
Finally, a source map will be appended to the CSS file when running in development mode using gulp-sourcemaps once again.
Install all the modules:
npm install gulp-sass gulp-postcss postcss-assets autoprefixer css-mqpacker cssnano --save-dev
Load them at the top of gulpfile.js
:
const ... sass = require('gulp-sass'), postcss = require('gulp-postcss'), assets = require('postcss-assets'), autoprefixer = require('autoprefixer'), mqpacker = require('css-mqpacker'), cssnano = require('cssnano'),
We can now export a new css
task at the end of gulpfile.js
. Note that the images
task is set as a dependency, because the postcss-assets
plugin may reference images during the build process:
// CSS processingfunction css() {
return gulp.src(src + 'scss/main.scss') .pipe(sourcemaps ? sourcemaps.init() : noop()) .pipe(sass({ outputStyle: 'nested', imagePath: '/images/', precision: 3, errLogToConsole: true }).on('error', sass.logError)) .pipe(postcss([ assets({ loadPaths: ['images/'] }), autoprefixer({ browsers: ['last 2 versions', '> 2%'] }), mqpacker, cssnano ])) .pipe(sourcemaps ? sourcemaps.write() : noop()) .pipe(gulp.dest(build + 'css/'));
}exports.css = gulp.series(images, css);
Note that .on('error', sass.logError)
ensures Sass outputs syntax errors to the console without stopping the Gulp task.
Save the file, add appropriate Sass .scss
files, and run the task from the command line:
gulp css
Step 7: Automate Tasks
We’ve been running one task at a time. We can run them all in one command by exporting a build
task in gulpfile.js
:
// run all tasksexports.build = gulp.parallel(exports.html, exports.css, exports.js);
The gulp.parallel()
method runs tasks at the same time. It can be combined with gulp.series()
to create complex dependency chains. In this example, exports.html
, exports.css
, and exports.js
are run in parallel, but each of those may have sequences of dependencies—including the images
task.
Save and enter gulp build
at the command line to execute all tasks.
Is this too much hard work? Gulp offers a .watch()
method that can monitor source files and run an appropriate task whenever a file is changed. It’s passed a set of files/folders to monitor, any options (unused here), and the task function to run (optionally within gulp.series()
and/or gulp.parallel()
methods).
Let’s export a new watch
task at the end of gulpfile.js
:
// watch for file changesfunction watch(done) {
// image changes gulp.watch(src + 'images/**/*', images);
// html changes gulp.watch(src + 'html/**/*', html);
// css changes gulp.watch(src + 'scss/**/*', css);
// js changes gulp.watch(src + 'js/**/*', js);
done();
}exports.watch = watch;
Gulp needs to know when a task function has completed. This is normally handled by returning a Gulp stream, but can optionally be a JavaScript Promise, event emitter, observable, child process, or a callback. Here, we’re using a callback named done()
to indicate that all watch()
tasks have been configured.
Rather than running gulp watch
immediately, let’s add a default task that can be executed by running gulp
without further arguments:
// default taskexports.default = gulp.series(exports.build, exports.watch);
Save gulpfile.js
and enter gulp
at the command line. Your images, HTML, CSS and JavaScript will be processed, then Gulp will remain active watching for updates and re-running tasks as necessary. Hit Ctrl/Cmd + C to abort monitoring and return to the command line.
Step 8: Profit!
Other plugins you may find useful:
- gulp-load-plugins: load all Gulp plugin modules without
require
declarations - gulp-preprocess: a simple HTML and JavaScript preprocess
- or gulp-less: the Less CSS pre-processor plugin
- gulp-stylus: the Stylus CSS pre-processor plugin
- gulp-size: displays file sizes and savings
- gulp-nodemon: uses nodemon to automatically restart Node.js applications when changes occur
Gulp tasks can run any JavaScript code or Node.js modules. They don’t necessarily need to be plugins—for example:
- browser-sync: automatically reload assets or refresh your browser when changes occur
- del: delete files and folders (perhaps clean your
build
folder at the start of every run)
Invest a little time and Gulp could save many hours of development frustration. The advantages:
- plugins are plentiful
- configuration using pipes is readable and easy to follow
gulpfile.js
can be adapted and reused in other projects- your total page weight can be reduced to improve performance
- you can simplify your deployment
- coding
gulpfile.js
is fun (well, more interesting than JSON configurations used by other task runners)
Useful links:
Applying the processes above to a simple website reduced the total weight by more than 50% or more. You can test your own results using page weight analysis tools.
Gulp continues to be a great option for automated task running and can revolutionize your workflow. I hope you found this guide useful and consider Gulp for your production process.