Seeing build tools in your nightmares? I was.

Al Romano - Apr 5 '19 - - Dev Community

Now, this is a story all about how
My build got flipped-turned upside down
And I'd like to take a minute
Just sit right there
I'll tell you how I saved my websites' build all while eating a pear.

In the project's node_modules init and raised
On VS Code was where I spent most of my days
Chillin' out maxin' relaxin' all cool
And just writtin some Gulpjs outside of the school
When a couple of pipes who were up to no good
Started making trouble in my build tool's hood
I got one little exception and my PM got scared
She said 'You're movin' away from Gulp to a better tool out their...'

Recently, I had the opportunity to refactor some old legacy sites. We have many of them and to keep things simple we use a standardized Gulp build template for a lot of the applications. I felt that it was time to revisit this build template since I made it back in 2017 and haven't really had time to update it much even though it's used regularly.

When doing some research I stumbled across Brunch.io and once I started to learn more, I was hooked. As someone who's used a lot of the build tools available (Gulp, Grunt, NPM, Parcel, Webpack, etc..) I was surprised about how simple it was to set up, configure and simply - build.

What was originally 350+ lines of gulpfile.js build glory was boiled down to a mere 39 lines inside of a brunch-config.js file!

Seriously. I'm not kidding! Let's break this down.

The Gulp Build File

This is the "massive" gulp.js build file I used for my microsite project.

/**
 * Created by Alexander Romano on 02/05/2017.
 * Front-End DevOps - Automate using GulpJS
 *
 * A little dependent heavy, but not without purpose...
 */

// Assigning modules to local variables
var gulp = require('gulp');
var less = require('gulp-less');
var syncDocs = require('browser-sync').create();
var browserSync = require('browser-sync').create();
var header = require('gulp-header');
var cleanCSS = require('gulp-clean-css');
var rename = require("gulp-rename");
var pkg = require('./package.json');
var merge = require('merge-stream');
var connect = require('gulp-connect');
var useref = require('gulp-useref');
var uglify = require('gulp-uglify');
var gulpIf = require('gulp-if');
var cssnano = require('gulp-cssnano');
var runSequence = require('run-sequence');
var imagemin = require('gulp-imagemin');
var cache = require('gulp-cache');
var del = require('del');
var sourcemaps = require('gulp-sourcemaps');

// Sets Up Rollbar Error Reporting
// Get a free account at https://rollbar.com/signup/
var rollbar = require('gulp-rollbar');
// Make sure to get your Token and Version from Rollbar account first
// Paste in the values for the specific project
var rollbarToken = "###";
var rollbarVersion = "###";
// Set Production URL for Rollbar
var rollbarURL = "https://projectname.netlify.com";

// Set banner content
var banner = ['/*!\n',
    ' * Virtually(Creative) - <%= pkg.title %> v<%= pkg.version %> (<%= pkg.homepage %>)\n',
    ' * Copyright ' + (new Date()).getFullYear(), ' <%= pkg.author %>\n',
    ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n',
    ' */\n',
    ''
].join('');

/**
 * BROWSER SYNC
 */
// Starts BrowserSync on the Compiled Dev Folder (APP)
gulp.task('browserSync:dev', function () {
    browserSync.init({
        server: {
            baseDir: 'app'
        },
    });
});

// Starts BrowserSync on the Compiled Dev Folder (DEMO)
gulp.task('browserSync:demo', function () {
    browserSync.init({
        server: {
            baseDir: 'demo'
        },
    });
});

/**
 * USEREF
 */
// Starts the DEV concat of all JS/CSS wrapped in Build Comments
// Pushes errors to Rollbar service to monitor during development
// Dumps the results in DEMO folder
gulp.task('useref:dev', function () {
    return gulp.src('app/*.html')
        .pipe(useref())
        .pipe(sourcemaps.init())
        .pipe(sourcemaps.identityMap())
        .pipe(gulpIf('app/js/*.js', uglify()))
        .pipe(rollbar({
            accessToken: rollbarToken,
            version: rollbarVersion,
            sourceMappingURLPrefix: 'http://localhost:3000'
        }))
        .pipe(gulpIf('app/css/*.css', cssnano()))
        .pipe(gulp.dest('demo/'));
});

// Starts the PROD concat of all JS/CSS wrapped in Build Comments
// Pushes errors to Rollbar service to monitor during production
// Dumps the results in the DIST folder
gulp.task('useref:prod', function () {
    return gulp.src('app/*.html')
        .pipe(useref())
        .pipe(sourcemaps.init())
        .pipe(sourcemaps.identityMap())
        .pipe(gulpIf('app/js/*.js', uglify()))
        .pipe(rollbar({
            accessToken: rollbarToken,
            version: rollbarVersion,
            sourceMappingURLPrefix: rollbarURL
        }))
        .pipe(gulpIf('app/css/*.css', cssnano()))
        .pipe(gulp.dest('dist/'));
});

/**
 * IMAGE OPTIMIZATION & CACHING
 */
// Finds and optimizes all images and caches results
// Only need to redo cache if new images are added after build
gulp.task('images:dev', function () {
    return gulp.src('app/img/**/*.+(png|jpg|jpeg|gif|svg)')
        // Caching images that ran through imagemin
        .pipe(cache(imagemin({
            interlaced: true
        })))
        .pipe(gulp.dest('demo/img'));
});

// Finds and optimizes all images and caches results
// Only need to redo cache if new images are added after build
gulp.task('images:prod', function () {
    return gulp.src('app/img/**/*.+(png|jpg|jpeg|gif|svg)')
        // Caching images that ran through imagemin
        .pipe(cache(imagemin({
            interlaced: true
        })))
        .pipe(gulp.dest('dist/img'));
});

/**
 * ASSET COPIES
 * FONTS & VIDEOS
 */

// Copies font's to DEMO folder
gulp.task('fonts:dev', function () {
    return gulp.src('app/fonts/**/*')
        .pipe(gulp.dest('demo/fonts'));
});

// Copies fonts to dist folder
gulp.task('fonts:prod', function () {
    return gulp.src('app/fonts/**/*')
        .pipe(gulp.dest('dist/fonts'));
});

// Copies vids (if any) to demo folder
gulp.task('vids:dev', function () {
    return gulp.src('app/vids/**/*')
        .pipe(gulp.dest('demo/vids'));
});

// Copies vids (if any) to dist folder
gulp.task('vids:prod', function () {
    return gulp.src('app/vids/**/*')
        .pipe(gulp.dest('dist/vids'));
});

/**
 * DEPENDANTS (NPM / BOWER)
 */

/**
 * CLEAN (Delete)
 */
// Cleans DIST folder
gulp.task('clean:prod', function () {
    return del.sync('dist');
});

// Cleans DEMO folder
gulp.task('clean:demo', function () {
    return del.sync('demo');
});

/**
 * MAIN BUILD TASKS
 */

// Main Dev task
// Runs Browser Sync with Watcher
gulp.task('dev', ['browserSync:dev'], function () {
    // Reloads the browser whenever HTML or JS files change
    gulp.watch('app/*.html', browserSync.reload);
    gulp.watch('app/js/**/*.js', browserSync.reload);
    gulp.watch('app/css/**/*.css', browserSync.reload);
});

// Main DEMO task
// Runs Browser Sync with Watcher
gulp.task('demo', ['browserSync:demo'], function () {
    // Reloads the browser whenever HTML or JS files change
    gulp.watch('demo/*.html', browserSync.reload);
    gulp.watch('demo/js/**/*.js', browserSync.reload);
    gulp.watch('demo/css/**/*.css', browserSync.reload);
});

// Main Dev Build task
// Builds Demo Folder by running all :dev tasks
gulp.task('demo:build', function (callback) {
    runSequence('clean:demo',
        ['useref:dev', 'fonts:dev', 'vids:dev', 'default'], 'images:dev', 'demo',
        callback
    );
});

// Main PROD Build task
// Builds Dist folder by running all :prod tasks
gulp.task('prod:build', function (callback) {
    runSequence('clean:prod',
        ['useref:prod', 'fonts:prod', 'vids:prod', 'default'], 'images:prod',
        callback
    );
});

// Default PROD Build task
// Builds Dist folder by running all :prod tasks
// typically invoked during an automated deployment
// Default task
gulp.task('default', ['prod:build']);
// Default "build" task
gulp.task('build', function (callback) {
    runSequence('prod:build', callback);
});

// Less task to compile the less files and add the banner
gulp.task('less', function () {
    return gulp.src('app/less/*.less')
        .pipe(less())
        .pipe(header(banner, {
            pkg: pkg
        }))
        .pipe(gulp.dest('css'))
        .pipe(browserSync.reload({
            stream: true
        }));
});

// Minify CSS
gulp.task('minify-css', function () {
    var vCBP = gulp.src('app/css/*.css')
        .pipe(cleanCSS({
            compatibility: 'ie8'
        }))
        .pipe(rename({
            suffix: '.min'
        }))
        .pipe(gulp.dest('css'))
        .pipe(browserSync.reload({
            stream: true
        }));

    var vCVS = gulp.src('vendor/*/css/*.css')
        .pipe(cleanCSS({
            compatibility: 'ie8'
        }))
        .pipe(rename({
            suffix: '.min'
        }))
        .pipe(gulp.dest('css'))
        .pipe(browserSync.reload({
            stream: true
        }));

    return merge(vCBP, vCVS);
});

// Minify JS
gulp.task('minify-js', function () {
    var vCBPjs = gulp.src('app/js/*.js')
        .pipe(uglify())
        .pipe(header(banner, {
            pkg: pkg
        }))
        .pipe(rename({
            suffix: '.min'
        }))
        .pipe(gulp.dest('js'))
        .pipe(browserSync.reload({
            stream: true
        }));

    var vCVendorjs = gulp.src('vendor/*/scripts/*.js')
        .pipe(uglify())
        .pipe(header(banner, {
            pkg: pkg
        }))
        .pipe(rename({
            suffix: '.min'
        }))
        .pipe(gulp.dest('js'))
        .pipe(browserSync.reload({
            stream: true
        }));

    return merge(vCBPjs, vCVendorjs);
});

// Copy Bootstrap core files from node_modules to vendor directory
gulp.task('bootstrap', function () {
    return gulp.src(['node_modules/bootstrap/dist/**/*', '!**/npm.js', '!**/bootstrap-theme.*', '!**/*.map'])
        .pipe(gulp.dest('vendor/bootstrap'));
});

// Copy jQuery core files from node_modules to vendor directory
gulp.task('jquery', function () {
    return gulp.src(['node_modules/jquery/dist/jquery.js', 'node_modules/jquery/dist/jquery.min.js'])
        .pipe(gulp.dest('vendor/jquery'));
});

// Copy Magnific Popup core files from node_modules to vendor directory
gulp.task('magnific-popup', function () {
    return gulp.src(['node_modules/magnific-popup/dist/*'])
        .pipe(gulp.dest('vendor/magnific-popup'));
});

// Copy ScrollReveal JS core JavaScript files from node_modules
gulp.task('scrollreveal', function () {
    return gulp.src(['node_modules/scrollreveal/dist/*.js'])
        .pipe(gulp.dest('vendor/scrollreveal'));
});

// Copy Font Awesome core files from node_modules to vendor directory
gulp.task('fontawesome', function () {
    return gulp.src([
            'node_modules/font-awesome/**',
            '!node_modules/font-awesome/**/*.map',
            '!node_modules/font-awesome/.npmignore',
            '!node_modules/font-awesome/*.txt',
            '!node_modules/font-awesome/*.md',
            '!node_modules/font-awesome/*.json'
        ])
        .pipe(gulp.dest('vendor/font-awesome'));
});

// Copy all dependencies from node_modules
gulp.task('copy', ['bootstrap', 'jquery', 'fontawesome', 'magnific-popup', 'scrollreveal']);

// Create browserSync task for Docs
gulp.task('syncDocs', function () {
    browserSync.init({
        server: {
            baseDir: 'docs/'
        }
    });
});

// Watch Task that watches for HTML/JS changes in Docs folder and reloads BrowserSync
gulp.task('docs', ['syncDocs'], function () {
    // Reloads the browser whenever HTML or JS files change
    gulp.watch('*.html', syncDocs.reload);
    gulp.watch('assets/*.js', syncDocs.reload);
    gulp.watch('assets/*.css', syncDocs.reload);
});

Wowza, 356 lines (including comments) just to build a single-page microsite.
What's going on here? Check out the comments within each section to get an idea.

The Brunch.io Difference

Comparatively, Brunch is simple. Brunch compiles, concats and (optionally) minifies your scripts and styles. It can also package JavaScript files into AMD or CommonJS modules. Brunch automatically applies plugins in the correct order to the right files - so with the right plugins, a .coffee file would be converted into a .js file and then minified, with no explicit setup necessary. To learn more check out the brunch.io documentation.

Brunch has a few conventions that help keep things simple - but you don’t have to follow all of them.

Firstly, Brunch asks you to specify a folder called assets that are directly copied into your output folder with no modifications.

Secondly, most Brunch projects have two separate JavaScript files - app.js, which contains your code, and vendor.js for all external libraries, including bower packages. This allows you to package your files into modules without affecting external libraries.

Using Brunch

The brunch command has two main commands - brunch build --production and brunch start.

Build runs the Brunch compilation process and immediately quits, conversely start compiles everything then waits for changes on any of your files, and then immediately compiles and updates the files.

Unlike Grunt or Gulp with plugins, Brunch caches your files, so after the initial compile, the start command is incredibly fast.

By default, minification is disabled. Brunch has a --production flag, which minifies the output files, removes source maps and disable the auto reload plugin.

Ok, there are a lot of things going on above in that Gulp.js file that we'll need to replace.

To do all of that, here is what our brunch-config.js turns out looking like,

module.exports = {
  files: {
    javascripts: {
      joinTo: {
        'vendor.js': [
          'vendor/jquery/1.11.3/jquery.min.js', // include specific file
          'vendor/jquery/mobile/touch/1.4.5/jquery.mobile.touch.min.js', // include specific file
          'vendor/bootstrap/3.3.5/bootstrap.min.js', // include specific file
          'vendor/global/js/custom.js', // include specific file
        ],
        'app.js': /^app/ // all code from 'app/'
      },
    },
    stylesheets: {
      joinTo: {
        'vendor.css': /^vendor/, //all code from 'vendor/'
        'app.css': /^app/ // all code from 'app/'
      }
    }
  },
  plugins: {
    cssnano: {
      autoprefixer: {
        add: true
      },
    }
  },
  swPrecache: {
    swFileName: 'sw.js',
    'autorequire': false,
    options: {
      staticFileGlobs: ['public/**/*.*'],
      stripPrefix: 'public/',
    }
  }
}

But wait, where's image minification and what's that swPrecache stuff?
Image minification is already done for you out of the box with imagemin-brunch package installed in the project.

Need to turn your app into a Progressive Web App? Add in swprecache-brunch to auto-generate your service-worker.js and offline cached assets.

Using Node Modules

NPM integration is enabled by default starting Brunch 0.2.3 so there's no additional setup! Simply npm install --save-dev your front-end packages as you normally would, require them in your app, and Brunch will figure out the rest! Usually, there is a package-brunch for what you need which auto-config's that item out of the box.

Package.json

To highlight the ease-of-use for Brunch, here is the package.json dependencies section to showcase what I mean. Once you install a -brunch package that's usually all you need to get it working!

 "devDependencies": {
    "auto-reload-brunch": "^2",
    "babel-brunch": "~6.0",
    "babel-preset-latest": "^6",
    "brunch": "^2.10.17",
    "cssnano-brunch": "^1.1.8",
    "imagemin-brunch": "^1.1.0",
    "sw-precache-brunch": "^2.0.1",
    "uglify-js-brunch": "^2"
  },
  "dependencies": {}
}

In Gulp's Defence...

In defence of Gulp, that gulp.js file above is rather sloppy and has lots of repeated code sections & verbose comments which easily could be removed and refactored into more succinct functions and then called upon when required.

I get that.

But, part of the point is rather than having to work through a large refactoring effort, I still only need 40 lines or less in Brunch to achieve my intended results. What if this wasn't my own project but another developer's I adopted and saw that massive Gulp file and was told to "make this better"?

These 40 Lines in brunch-config.js I'm sure would take me less time to type than even thinking about how I'd like to go about refactoring that gulp code above.

Still, As we mentioned earlier, Brunch simplifies the setup process tremendously by making some basic assumptions about the setup of your project. Unfortunately, if your project is exceptionally unique it might be quite a headache trying to customize Brunch to work with what you’ve got.

Conversely, Gulp really shines here. Gulp's configuration file is definitely more demanding than Brunch’s, but it offers pretty much-unlimited flexibility. You can control just about every aspect of the task runner through the many options made available to you in the Gulp API and CLI documentation. And, of course, those 2,800+ plugins don’t hurt either...


How do you guys handle some of your more seemingly simple builds? Could this be done in even fewer lines using Webpack or something else entirely? Let me know! Would love to learn how you guys go about building some of your pipelines.

If anyone has questions, feel free to reach out!

Featured Photo Credit, Oomph Inc - 2017

.