I hope the answer to that question is a very loud, emphatic, “YES!”  I like gulp!  I want to use it!  And I’ll confess up front that I’m still very green with gulp.  I know there are a lot of things it can probably do that I’m not taking advantage of.  But, I feel like my development experience has taken a step backwards compared to what I had with ASP.NET Bundling & Minification.  I feel like gulp has just added more development friction to my day.  Someone, please tell me that I’m doing it wrong.

[more]

Apologies in Advance…

This post was not meant to be a rant.  Unfortunately, as I wrote it, my frustration definitely steered it in that direction at times.  As I’ll say again and again, I do not hate gulp.  I actually rather enjoyed working with it.  It can do some very cool things.  I love that I can use it the same way for my web apps as I can for my Cordova projects.  My only complaint is that I had to work with it as much as I did, but more on that below.  For now, please excuse my grumpiness, and feel free to set me right in the comments. 

Bundling & Minification

I suppose it would make sense to start not with gulp, but with the old solution that’s no longer supported in new versions of ASP.NET: ASP.NET Bunding & Minification/Web Optimizations (plus a bit of Web Essentials).  Perhaps if I explain my workflow with that, then someone can tell me what I’m missing to achieve something similar in gulp with less code than I’m currently using. 

With the old solution, I would create a BundleConfig file, like this:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new StyleBundle("~/Content/css")
                //Vendor styles
                .Include("~/css/vendor/bootstrap-custom.css")
                //App styles
                .Include("~/css/app/layout.css")
                .IncludeDirectory("~/css/app/", "*.css")
            );

        bundles.Add(new StyleBundle("~/Content/css").Include(
                  "~/Content/bootstrap.css",
                  "~/Content/ubuntu.theme.css",
                  "~/Content/site.css"));
    }
}

During development, I would make a change to one of my files, be it JS, CSS, or even LESS, and I could instantly see the change in my browser. 

GulpVsWebOptimization

No waiting, no lag, just make a change, save the file, and BAM, that change is now ready for me to see in my browser.  And this all Just Works right out of the box.  Aside from creating my BundleConfig, I didn’t have to write a single line of code, and I usually never had to touch my bundle config ever again.

Gulp

With gulp, I don’t get this out of the box.  Instead, I have to write a build script.  And I can’t just write a build script, I have to install numerous gulp plug-ins, then use them in my build script.  Heck, I even need a plugin to manage my plugins!  I also need to separate out my config, because gulp needs a lot of config (at least the way I did it, which hopefully is wrong), and it gets very noisy very quickly. 

So, I wrote a gulp file, modeled after numerous examples I’ve seen (and I’ve seen too many to count at this point), and after a bit of trial and error getting the necessary plugins to work correctly together (I’m looking at you, gulp-sourcemaps and gulp-minify-css!), I had something that was close to the experience I had before.  To keep things fast, I had to process my vendor styles and scripts separately from my application styles scripts.  I’m ok with that, I suppose.  It’s two extra requests for the end-user (now they need app.css/.js and vendor.css/.js), but I’m not too worried about that since the app files will be much smaller.

So, in my gulp file, I have separate tasks for processing my vendor and app LESS/CSS, and my vendor and app JS.  And I have separate ‘watch’ tasks for each, too, so that only the minimum amount of processing has to happen when I make a change.   And everything emits a nice sourcemap, so I can tell where a style came from, where a console.log came from, etc. 

Here is my final script, including the config file:

//gulp.config.js
'use strict';
var module = module || {};
module.exports = function () {

    var temp = "./tmp/";

    var config = {
        temp: temp,
        less: {
            vendor: {
                src: ['./css/bootstrap-custom.less'],
                output: temp + 'vendor'
            },
            app: {
                src: ['./Features/**/*.less'],
                output: temp + 'app'
            }
        },
        css: {
            vendor: {
                minFile: 'vendor.min.css',
                src: [temp + 'vendor/**/*.css']
            },
            app: {
                minFile: 'app.min.css',
                src: [temp + '/app/**/*.css']
            },
            destination: './css'
        },
        js: {
            vendor: {
                minFile: 'vendor.min.js',
                src: [
                    './js/angular/angular.js'
                ]
            },
            app: {
                minFile: 'app.min.js',
                src: ['./Features/**/*.js'],
            },
            destination: './js'
        }
    };

    return config;
};

//gulpfile.js
/// <binding ProjectOpened='watch, default' />
var gulp = require('gulp');
var del = require('del');
var $ = require('gulp-load-plugins')({ lazy: false });

var config = require('./gulp.config')();

gulp.task('default', ['styles', 'scripts']);

gulp.task('watch', function() {
    gulp.watch(config.less.vendor.src, ['deploy-vendor-styles']);
    gulp.watch(config.less.app.src, ['deploy-app-styles']);
    gulp.watch(config.js.vendor.src, ['deploy-vendor-scripts']);
    gulp.watch(config.js.app.src, ['deploy-app-scripts']);
});

gulp.task('styles', ['deploy-vendor-styles', 'deploy-app-styles']);

gulp.task('deploy-vendor-styles', ['minify-vendor-styles'], function () {
    log('Deploying vendor styles into app...');

    return gulp.src(config.temp + config.css.vendor.minFile + "*")
        .pipe(gulp.dest(config.css.destination));
});

gulp.task('deploy-app-styles', ['minify-app-styles'], function () {
    log('Deploying app styles into app...');

    return gulp.src(config.temp + config.css.app.minFile + "*")
        .pipe(gulp.dest(config.css.destination));
});

gulp.task('minify-vendor-styles', ['compile-vendor-less'], function() {
    return minifyStyles(config.css.vendor.src, config.css.vendor.minFile, config.temp);
});

gulp.task('minify-app-styles', ['compile-app-less'], function () {
    return minifyStyles(config.css.app.src, config.css.app.minFile, config.temp);
});

gulp.task('compile-vendor-less', ['clean-styles'], function() {
    return compileLess(config.less.vendor.src, config.less.vendor.output);
});

gulp.task('compile-app-less', ['clean-styles'], function() {
    return compileLess(config.less.app.src, config.less.app.output);
});

gulp.task('clean-styles', function (done) {
    clean(config.temp + '/**/*.css*', done);
});

gulp.task('clean-scripts', function (done) {
    clean(config.temp + '/**/*.js*', done);
});

gulp.task('scripts', ['deploy-vendor-scripts', 'deploy-app-scripts']);

gulp.task('deploy-vendor-scripts', ['minify-vendor-js'], function() {
    log('Deploying vendor scripts into app...');

    return gulp.src(config.temp + config.js.vendor.minFile + "*")
        .pipe(gulp.dest(config.js.destination));
});

gulp.task('deploy-app-scripts', ['minify-app-js'], function () {
    log('Deploying app scripts into app...');

    return gulp.src(config.temp + config.js.app.minFile + "*")
        .pipe(gulp.dest(config.js.destination));
});

gulp.task('minify-vendor-js', ['clean-scripts'], function () {
    return minifyScripts(config.js.vendor.src, config.js.vendor.minFile, config.temp);
});

gulp.task('minify-app-js', ['clean-scripts'], function () {
    return minifyScripts(config.js.app.src, config.js.app.minFile, config.temp);
});

///////////////////////////////////
//END OF GULP TASKS
///////////////////////////////////
function clean (path, done) {
    log('Cleaning: ' + $.util.colors.blue(path));
    del(path, done);
}

function log(msg, color) {
    color = color || 'blue';
    if (typeof (msg) === 'object') {
        for (var item in msg) {
            if (msg.hasOwnProperty(item)) {
                $.util.log($.util.colors[color](msg[item]));
            }
        }
    } else {
        $.util.log($.util.colors[color](msg));
    }
}

function errorHandler(err) {
    log(err.message, 'red');
    this.emit('end');
}

function minifyStyles(sourceFile, minFile, destination) {
    log('Minimizing \'' + sourceFile + '\' to ' + minFile + '...');

    return gulp.src(sourceFile)
        .pipe($.plumber({ errorHandler: errorHandler }))
        .pipe($.sourcemaps.init({ loadMaps: true, debug: true }))
        .pipe($.autoprefixer({ browsers: ['last 2 version', '> 5%'] }))
        .pipe($.minifyCss())
        .pipe($.concat(minFile))
        .pipe($.sourcemaps.write('.'))
        .pipe(gulp.dest(destination));
}

function compileLess(src, destination) {
    log('Processing less files from \'' + src + '\' into \'' + destination + '\'...');

    return gulp.src(src)
        .pipe($.plumber({ errorHandler: errorHandler }))
        .pipe($.sourcemaps.init())
        .pipe($.less())
        .pipe($.sourcemaps.write('.'))
        .pipe(gulp.dest(destination));
}

function minifyScripts(source, minFile, destination) {
    log('Minifying \'' + source + '\' into \'' + minFile + '\'...');

    return gulp.src(source)
          .pipe($.plumber({ errorHandler: errorHandler }))
        .pipe($.sourcemaps.init({ loadMaps: true }))
        .pipe($.uglify())
        .pipe($.concat(minFile))
        .pipe($.sourcemaps.write('.'))
        .pipe(gulp.dest(destination));
}

And I really, really hate this.  Why?  Not because I hate gulp.  I like gulp.  I hate this script because it was a “necessary” prerequisite to doing useful work.  And it was a non-trivial amount of work.  A very non-trivial amount.  I’m hoping someone can tell me that there is a better way.

The Problem

Again, let me say that I do like gulp.  As a build tool, I have (mostly) nice things to say about it.  If I’m going to write a build script with JS, I’m going to use it!  My problem with my gulp script is this: it is code that has *zero* business value.  It’s infrastructure which supports delivering business value, but it delivers none itself.   My gulp script does nothing interesting.  It does nothing clever.  And yet, it still has a ton of JS in it.  A ton of JS to solve uninteresting problems, and it contributes no business value. 

Let’s consider an example.  Let’s say I’m being paid to build a web app that will allow order takers to send out notices to a remote warehouse as a new order comes in.  Let’s say I have only a few hours I can spend on it right now.  I could do one of two things:

1) “I have this form somewhat working.  You can send out sales notices, but it’s slow because I’m not bundling anything.”  I focused on solving the frickin’ problem.  The feature is usable, though certainly not perfect.  But, I have contributed business value.

2) “Oh, man, I made great progress today!  I wrote the build script we need to make sure the JS and CSS are going to be combined and minified and all that jazz, that way our users will get things super fast!  Huh? The form??  Didn’t you hear what I said?  I made an awesome build script!  I didn’t have time to even start on your form for sending out sales notices!!  But don’t worry, once I have time to work on that form, it’ll be really fast!”  I wasted a ton of time on a problem that has no business value.  I failed to deliver even a partially-usable feature.  The business user doesn’t care about bundling, minification, gulp, grunt, or any of that.  He cares about getting his problems solved.  And I’ve failed him.

Now, some amount of infrastructure and setup is inevitable in a project.  It’s a necessary cost.  But I want to minimize that cost.  It’s 20-fricking-15.  We should be working at a higher level of abstraction than this.  The whole “Convention over Configuration” movement had this right.  Let me focus on solving the problems that matter, the problems whose solutions add business value!  All the uninteresting things should Just Work as long as I don’t need to do anything goofy.  Gulp, for me as an ASP.NET developer, feels like a step backwards.  Now I have to write a lot of code to do something that, previously, Just Worked.   

The Solution… ??

I hope I’ve made a mountain out of a mole hill.  I hope there’s some minimalist gulp script that will do everything I need with little to no effort.  It seems the entire web development community is moving in this direction.  Whether it’s gulp, grunt, webpack, or some new tool that was just invented today (and will be obsolete tomorrow!), the trend definitely seems to be in the direction of writing build scripts in JS.   Surely we can’t all be writing this much JS to do something as simple as packaging up our app?

So, am I doing it wrong?  Should I really have to write this much code to get my JS and CSS bundled up correctly?  Is everyone else doing this, too??  Are we all writing build applications now??

Is there a better way to do this?  Or is there some alternative that I’ve completely overlooked that makes all this pointless? Is there some super-gulpfile or plug-in that I should be using instead?

Someone please, please tell me that I’m doing it wrong.