Jul 1, 2016

tl;dr

Grunt gets the job done… but a giant config file? Gulp is by far my build tool of choice. It is so much more pleasant to write functions!

My gulpfile.js template

For the handful of angular modules I have recently worked on I’ve been using the following gulpfile with relatively minor tweaks. It runs my unit & e2e tests (I have landed on Karma and Jasmine as my preferred options), builds, minifies, serves, and can kick off a task used for travis to validate my builds (ensure I properly build & minify before merging to master).

'use strict';

var gulp = require('gulp'),
    gutil = require('gulp-util'),
    filesize = require('gulp-filesize'),
    uglify = require('gulp-uglify'),
    rename = require('gulp-rename'),
    concat = require('gulp-concat'),
    jshint = require('gulp-jshint'),
    stylish = require('jshint-stylish'),
    del = require('del'),
    browserSync = require('browser-sync'),
    reload = browserSync.reload,
    templateCache = require('gulp-angular-templatecache'),
    less = require('gulp-less'),
    path = require('path'),
    gulpProtractorAngular = require('gulp-angular-protractor'),
    KarmaServer = require('karma').Server,
    shell = require('gulp-shell');

// vars for finding directories
var match = {
  recurse: '**/*'
};

var root = './',
    src = './src/',
    dist = './dist/',
    tmp = './.tmp/',
    tmpBuild = tmp + 'build/',
    test = './test/',
    testRelative = '/test/',
    demos = test + 'manual/';

var srcAll = src + match.recurse,
    distAll = dist +match.recurse,
    demoAll = demos + match.recurse,
    tmpAll = tmpBuild + match.recurse;

var srcJS = src + match.recurse + '.js',
    srcView = src + '/views/'+ match.recurse + '.html',
    srcLess = src + '/less/' + match.recurse + '.less';

var outputJS = 'angular-key-value-editor.js',
    outputTpl = 'compiled-templates.js';

var buildSource = [
  src + '*.js',
  src + 'directives/**/*.js',
  src + 'services/**/*.js'
];

var angularModuleName = 'key-value-editor';

var protocol = 'http://',
    host = 'localhost',
    serverPort = 9005,
    baseUrl = protocol + host + ':' + serverPort;

var concatSource = function(outputDest) {
  return gulp
          .src(buildSource)
          .pipe(concat(outputJS))
          .pipe(filesize())
          .pipe(gulp.dest(outputDest || dist));
};

var minifyDist = function(outputDest) {
  return gulp
          .src(dist + outputJS)
          .pipe(uglify().on('error', gutil.log))
          .pipe(rename({ extname: '.min.js' }))
          .pipe(filesize())
          .pipe(gulp.dest(outputDest || dist));
};

var cacheTemplates = function(outputDest) {
  return gulp
          .src(srcView)
          .pipe(templateCache({
            module: angularModuleName
          }))
          .pipe(rename(outputTpl))
          .pipe(filesize())
          .pipe(gulp.dest(outputDest || dist));
};

var buildCSS = function(outputDest) {
  return gulp
          .src(srcLess)
          .pipe(less({
            paths: [ path.join(__dirname, 'less', 'includes') ]
          }))
          .pipe(gulp.dest(outputDest || dist));
};

gulp.task('clean', function() {
  return del([distAll, tmpAll], function(err, paths) {
    return gutil.log('cleaned files/folders:\n', paths.join('\n'), gutil.colors.green());
  });
});


gulp.task('jshint', function() {
  return gulp
          .src(srcJS)
          .pipe(jshint())
          .pipe(jshint.reporter(stylish));
});

gulp.task('templates', ['clean'], function () {
  return cacheTemplates();
});

gulp.task('less', ['clean'], function () {
  return buildCSS();
});

gulp.task('build', ['clean','templates', 'jshint', 'less'], function () {
  return concatSource();
});

gulp.task('min', ['build', 'templates'], function() {
  return minifyDist();
});

gulp.task('min-and-reload', ['min'], reload);

gulp.task('serve', function() {
  // https://www.browsersync.io/docs/options
  browserSync({
    port: serverPort,
    server: {
      baseDir: root
    }
   });

   // TODO: live-reloading for demo not working yet.
   gulp.watch([srcAll, distAll, demoAll], ['min-and-reload']);
});


gulp.task('_tmp-build', function() {
  return concatSource(tmpBuild);
});
gulp.task('_tmp-templates', function() {
  return cacheTemplates(tmpBuild);
});

gulp.task('_tmp-less', function() {
  return buildCSS(tmpBuild);
});

gulp.task('_tmp-min', ['_tmp-build', '_tmp-templates', '_tmp-less'], function() {
  return minifyDist(tmpBuild);
});

// at present this task exists for travis to use to before
// running ./validate.sh to diff our dist against ./.tmp/build
// and validate that templates have been cached, js minified, etc.
gulp.task('prep-diff', ['_tmp-min'], function() {
  // nothing here atm.
});

gulp.task('validate-dist', ['prep-diff'], function() {
  // validation script to verify ./dist and ./tmp/build are equals
  shell.task([
    './validate.sh'
  ])();
});

gulp.task('test-e2e', ['serve'], function(callback) {
    gulp
        .src(['example_spec.js'])
        .pipe(gulpProtractorAngular({
            configFile: test + 'protractor.conf.js',
            // baseUrl is needed for tests to navigate via relative paths
            args: ['--baseUrl', baseUrl],
            debug: false,
            autoStartStopServer: true
        }))
        .on('error', function(e) {
            console.log(e);
        })
        .on('end', callback);
});

// for integration testing, uses phantomJS
gulp.task('test-unit', function(done) {
    new KarmaServer({
      configFile:  __dirname  + testRelative + 'karma.conf.js',
      port: serverPort
      // browsers: ['PhantomJS'] - try the firefox default?
    }, done).start();
});

// run all the tests, unit first, then e2e
gulp.task('test', ['test-unit', 'test-e2e'], function() {
  // just runs the other tests
});

// for development, uses Chrome
// equivalent task to `test-unit`, but long running, watching file changes
gulp.task('tdd', function(done) {
  new KarmaServer({
    configFile: __dirname + testRelative + 'karma.conf.js',
    autoWatch: true,
    singleRun: false,
    port: serverPort
  }, done).start();
});

gulp.task('default', ['min', 'serve']);