React and be thankful A blog about building (reactive) web applications

Building with Gulp 3 and 4 (Part 1: examples)

Who are you Gulp?

Like Grunt, Gulp is also a task runner but is designed to build applications using streams: Gulp is "the streaming build system" to "automate and enhance your workflow".

Gulp was first released in July 2013 (v0.0.1), v3.0.0 was released in December 2013. Current version is 3.8.x released in June 2014 and Gulp 4 is now almost ready. Before talking about what Gulp is made of, let's look at a few examples to understand Gulp's syntax.

Gulp 3

Providing you already have a package.json file:

$ npm install -g gulp
$ npm install --save-dev gulp gulp-jshint gulp-concat rimraf

We installed the following plugins; gulp-concat, gulp-jshint and rimraf. Let's create a gulpfile.js alongside our package.json:

////////////////////
// Gulp 3 example //
////////////////////

var gulp   = require('gulp');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var del    = require('rimraf');

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

gulp.task('buildJs', ['clean'], function () {
    return gulp.src('src/**/*.js')
        .pipe(jshint())
        .pipe(concat('app.js'))
        .pipe(gulp.dest('build'));
});

gulp.task('copyAssets', ['clean'], function () {
    return gulp.src('src/assets/**/*.*')
        .pipe(gulp.dest(build/assets));
});

gulp.registerTask('build', ['buildJs', 'copyAssets']);

Gulp 3 with run-sequence

In Gulp 3, we have to specify task depencies so our tasks are executed in the right order (with maximum concurrency). In the above example, we want to clean our build directory and then build our application. For each task, we have to add 'clean' as a dependency which makes our build repetitive and not so practical. What if I want to re-compile JavaScript files without running clean? Rather than defining dependencies, it is preferable to define each task independently and then create orchestration tasks. In Gulp 3, we could use gulp.start() but we would quickly enter a callback hell. Instead We can use run-sequence to avoid to have to specify dependencies and make our build more maintainable and composable.

$ npm install --save-dev run-sequence
//////////////////////////////////////
// Gulp 3 example with run-sequence //
//////////////////////////////////////

var gulp        = require('gulp');
var jshint      = require('gulp-jshint');
var concat      = require('gulp-concat');
var del         = require('rimraf');
var runSequence = require('run-sequence');

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

gulp.task('buildJs', function () {
    return gulp.src('src/**/*.js')
        .pipe(jshint())
        .pipe(concat('app.js'))
        .pipe(gulp.dest('build'));
});

gulp.task('copyAssets', function () {
    return gulp.src('src/assets/**/*.*')
        .pipe(gulp.dest('build/assets'));
});

gulp.registerTask('build', function(done) {
    runSequence('clean', ['buildJs', 'copyAssets'], done);
});

Gulp 4

In Gulp 4, the above example would be:

////////////////////
// Gulp 4 example //
////////////////////

var gulp   = require('gulp');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var del    = require('rimraf');

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

gulp.task('buildJs', function () {
    return gulp.src('src/**/*.js')
        .pipe(jshint())
        .pipe(concat('app.js'))
        .pipe(gulp.dest('build'));
});

gulp.task('copyAssets', function () {
    return gulp.src('src/assets/**/*.*')
        .pipe(gulp.dest('build/assets'));
});

gulp.registerTask('build', gulp.series(
    'clean',
    gulp.parallel('buildJs', 'copyAssets');
));

Splitting the gulpfile

When a build increases in complexity and size, it is a good idea to leverage Node modules for spliting up our gulpfile. There are two ways one can split a gulpfile: by tasks or by streams. For an idea of how to split by task, you can look at how it can be done in Grunt, the idea is the same. I prefer to split by streams so all the task registration is done in the gulpfile and it leaves room for reusing streams or parts of a stream. It feels anyway a lot more functional:

├── src
|   └── ...
├── streams
|   ├── javascript.js
|   └── assets.js
├── gulpfile.js
└── package.json

javascript.js

var gulp = require('gulp');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');

module.exports = function () {
    return gulp.src('src/**/*.js')
        .pipe(jshint())
        .pipe(concat('app.js'))
        .pipe(gulp.dest('build'));
};

assets.js

var gulp = require('gulp');

module.exports = function () {
    return gulp.src('src/assets/**/*.*')
        .pipe(gulp.dest('build/assets'));
};

gulpfile.js

var gulp         = require('gulp');
var del          = require('rimraf');
// Streams
var jsStream     = require('./streams/javascript');
var assetsStream = require('./streams/assets')

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

gulp.task('buildJs', jsStream);
gulp.task('copyAssets', assetsStream);

Something wrong? Fix it on Github!