Advanced Gulp File

With gulp starting to find itself into my everyday workflow - I've started to understand its quirks and twists, and how to get along with it. My baseline gulfile.js has become a lot neater and advanced in its functionality that the one I originally developed back in March.

Along with Gulp, I have also started integrating Bower, a front-end package manager, into my workflow. I now use Bower to download the assets and plugins and gulp to compile, concatenate and minify them. Luigi is installed by default - but I also use it for Fancybox, jQuery and other front-end packages.

The full code including the bower.json and package.json files can be found over on Github.

gulpfile.js #

Using Gulp has replaced using the built in Sass command line compiler. We opted for Gulp over Grunt as it was easier to grasp from a front end perspective. I was able to pick up Gulp in an evening - whereas Grunt scared the bejesus out of me.

The gulpfile.js is broken down into noticeable chunks each chunk is explained below

File paths #

To start of I declare several paths - these get used throughout the gulpfile and mean you only have to define them in one place. For consistency, make sure you end your paths with a /

var basePaths = {
    src: 'app/assets/',
    dest: 'public/assets/',
    bower: 'bower_components/'
};
var paths = {
    images: {
        src: basePaths.src + 'images/',
        dest: basePaths.dest + 'images/min/'
    },
    scripts: {
        src: basePaths.src + 'js/',
        dest: basePaths.dest + 'js/min/'
    },
    styles: {
        src: basePaths.src + 'sass/',
        dest: basePaths.dest + 'css/min/'
    },
    sprite: {
        src: basePaths.src + 'sprite/*'
    }
};
var appFiles = {
    styles: paths.styles.src + '**/*.scss',
    scripts: [paths.scripts.src + 'scripts.js']
};
var vendorFiles = {
    styles: '',
    scripts: ''
};
var spriteConfig = {
    imgName: 'sprite.png',
    cssName: '_sprite.scss',
    imgPath: paths.images.dest + 'sprite.png' // Gets put in the css
};

Plugins #

Gulp is defined along with some other plugins. gutil is a gulp utility plugin, enabling messages, errors and allows the ability to pass in flags. es is used to manipulate the result of the event stream. Lastly, we load Jack Franklin's gulp-load-plugins which searches the package.json file for gulp plugins so you don't have to specify them individually.

var gulp = require('gulp');
var es = require('event-stream');
var gutil = require('gulp-util');
var plugins = require("gulp-load-plugins")({
    pattern: ['gulp-*', 'gulp.*'],
    replaceString: /\bgulp[\-.]/
});

Development Variables and Functions #

Next, I set up some variables I can use throughout the rest of the file, I then check to see if the --dev flag exists - if so, the variables are overwritten. This gives me the ability to have compiled, compressed sass, or expanded source-mapped sass.

For example, if you wished to have an expanded, source-mapped sass, you would run

$ gulp --dev

The isProduction variable is a generic true/false for if the --dev flag is present.

var isProduction = true;
var sassStyle = 'compressed';
var sourceMap = false;
if(gutil.env.dev === true) {
    sassStyle = 'expanded';
    sourceMap = true;
    isProduction = false;
}

I have also defined a changeEvent function - something which gets fired whenever a file changes. This outputs what file it was and what happened to it

var changeEvent = function(evt) {
    gutil.log('File', gutil.colors.cyan(evt.path.replace(new RegExp('/.*(?=/' + basePaths.src + ')/'), '')), 'was', gutil.colors.magenta(evt.type));
};

CSS Task #

With the CSS task I had 2 functions I wanted to achieve: Compile the Sass and concatenate/compress that with any other stylesheets I needed to include. Originally, I had these as two separate tasks, with the concatenation relying on the compiler - however, it would still try and concatenate before the sass was fully compiled, which meant the concatenated CSS didn't contain the latest compiled Sass.

Using the eventstream (es) plugin, I was able to find a solution by compiling the Sass and then paasing that to the concatenation plugin. Once concatenated with any other CSS sheets, the file is passed through an autoprefixer and, if isProduction is true, a Media Query combiner and is finally minified.

gulp.task('css', function(){
    var sassFiles = gulp.src(appFiles.styles)
    .pipe(plugins.rubySass({
        style: sassStyle, sourcemap: sourceMap, precision: 2
    }))
    .on('error', function(err){
        new gutil.PluginError('CSS', err, {showStack: true});
    });
    return es.concat(gulp.src(vendorFiles.styles), sassFiles)
        .pipe(plugins.concat('style.min.css'))
        .pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
        .pipe(isProduction ? plugins.combineMediaQueries({
            log: true
        }) : gutil.noop())
        .pipe(isProduction ? plugins.cssmin() : gutil.noop())
        .pipe(plugins.size())
        .pipe(gulp.dest(paths.styles.dest));
});

Javascript/Scripts Task #

The scripts one is not as complex - it's a concatenation of scripts and then uglifying the files (only if the --dev flag is omitted)

gulp.task('scripts', function(){
    gulp.src(vendorFiles.scripts.concat(appFiles.scripts))
        .pipe(plugins.concat('app.js'))
        .pipe(gulp.dest(paths.scripts.dest))
        .pipe(isProduction ? plugins.uglify() : gutil.noop())
        .pipe(plugins.size())
        .pipe(gulp.dest(paths.scripts.dest));
});

CSS Sprite Task #

The last function of the gulpfile is to create a sprite. This allows me to drop in png files and it create a sprite with the right dimensions and corresponding variables. With this one, I omit the creation of the sprite mixins, as I have custom ones included in Luigi. Once I have included the generated _sprite.scss, I can type @include spritesmith(filename); for it to get the right background, dimensions and co-ordinates:

gulp.task('sprite', function () {
    var spriteData = gulp.src(paths.sprite.src).pipe(plugins.spritesmith({
        imgName: spriteConfig.imgName,
        cssName: spriteConfig.cssName,
        imgPath: spriteConfig.imgPath,
        cssVarMap: function (sprite) {
            sprite.name = 'sprite-' + sprite.name;
        }
    }));
    spriteData.img.pipe(gulp.dest(paths.images.dest));
    spriteData.css.pipe(gulp.dest(paths.styles.src));
});

Default and Watch Tasks #

The rest of the file is comprised of the default and watch tasks. Within the watch, I use the changeEvent function to fire when a file gets updated. I also ensure all three of the functions above get run before watching commences:

gulp.task('watch', ['sprite', 'css', 'scripts'], function(){
    gulp.watch(appFiles.styles, ['css']).on('change', function(evt) {
        changeEvent(evt);
    });
    gulp.watch(paths.scripts.src + '*.js', ['scripts']).on('change', function(evt) {
        changeEvent(evt);
    });
});
gulp.task('default', ['css', 'scripts']);

package.json #

This is the json file needed for all of the above functions - if you are using this gulp file, copy and paste the below into yours and run:

npm install

From there, you should be pretty much good to go:

{
    "devDependencies": {
        "event-stream": "^3.1.5",
        "gulp": "^3.5.6",
        "gulp-autoprefixer": "0.0.6",
        "gulp-combine-media-queries": "0.0.1",
        "gulp-concat": "^2.2.0",
        "gulp-cssmin": "^0.1.5",
        "gulp-load-plugins": "^0.4.0",
        "gulp-rename": "^1.2.0",
        "gulp-ruby-sass": "^0.4.0",
        "gulp-size": "^0.3.1",
        "gulp-uglify": "^0.2.1",
        "gulp-util": "^2.2.14",
        "gulp.spritesmith": "^0.3.0"
    }
}

My Gulp File #

Here is the whole gulpfile.js:

var basePaths = {
    src: 'app/assets/',
    dest: 'public/assets/',
    bower: 'bower_components/'
};
var paths = {
    images: {
        src: basePaths.src + 'images/',
        dest: basePaths.dest + 'images/min/'
    },
    scripts: {
        src: basePaths.src + 'js/',
        dest: basePaths.dest + 'js/min/'
    },
    styles: {
        src: basePaths.src + 'sass/',
        dest: basePaths.dest + 'css/min/'
    },
    sprite: {
        src: basePaths.src + 'sprite/*'
    }
};
var appFiles = {
    styles: paths.styles.src + '**/*.scss',
    scripts: [paths.scripts.src + 'scripts.js']
};
var vendorFiles = {
    styles: '',
    scripts: ''
};
var spriteConfig = {
    imgName: 'sprite.png',
    cssName: '_sprite.scss',
    imgPath: paths.images.dest + 'sprite.png' // Gets put in the css
};
/*
    Let the magic begin
*/
var gulp = require('gulp');
var es = require('event-stream');
var gutil = require('gulp-util');
var plugins = require("gulp-load-plugins")({
    pattern: ['gulp-*', 'gulp.*'],
    replaceString: /\bgulp[\-.]/
});
// Allows gulp --dev to be run for a more verbose output
var isProduction = true;
var sassStyle = 'compressed';
var sourceMap = false;
if(gutil.env.dev === true) {
    sassStyle = 'expanded';
    sourceMap = true;
    isProduction = false;
}
var changeEvent = function(evt) {
    gutil.log('File', gutil.colors.cyan(evt.path.replace(new RegExp('/.*(?=/' + basePaths.src + ')/'), '')), 'was', gutil.colors.magenta(evt.type));
};
gulp.task('css', function(){
    var sassFiles = gulp.src(appFiles.styles)
    .pipe(plugins.rubySass({
        style: sassStyle, sourcemap: sourceMap, precision: 2
    }))
    .on('error', function(err){
        new gutil.PluginError('CSS', err, {showStack: true});
    });
    return es.concat(gulp.src(vendorFiles.styles), sassFiles)
        .pipe(plugins.concat('style.min.css'))
        .pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
        .pipe(isProduction ? plugins.combineMediaQueries({
            log: true
        }) : gutil.noop())
        .pipe(isProduction ? plugins.cssmin() : gutil.noop())
        .pipe(plugins.size())
        .pipe(gulp.dest(paths.styles.dest));
});
gulp.task('scripts', function(){
    gulp.src(vendorFiles.scripts.concat(appFiles.scripts))
        .pipe(plugins.concat('app.js'))
        .pipe(gulp.dest(paths.scripts.dest))
        .pipe(isProduction ? plugins.uglify() : gutil.noop())
        .pipe(plugins.size())
        .pipe(gulp.dest(paths.scripts.dest));
});
/*
    Sprite Generator
*/
gulp.task('sprite', function () {
    var spriteData = gulp.src(paths.sprite.src).pipe(plugins.spritesmith({
        imgName: spriteConfig.imgName,
        cssName: spriteConfig.cssName,
        imgPath: spriteConfig.imgPath,
        cssVarMap: function (sprite) {
            sprite.name = 'sprite-' + sprite.name;
        }
    }));
    spriteData.img.pipe(gulp.dest(paths.images.dest));
    spriteData.css.pipe(gulp.dest(paths.styles.src));
});
gulp.task('watch', ['sprite', 'css', 'scripts'], function(){
    gulp.watch(appFiles.styles, ['css']).on('change', function(evt) {
        changeEvent(evt);
    });
    gulp.watch(paths.scripts.src + '*.js', ['scripts']).on('change', function(evt) {
        changeEvent(evt);
    });
});
gulp.task('default', ['css', 'scripts']);

For an up to date version of the file, head over to the git repository.