Frontend Builds 2: readable and pluggable gulpfiles

Frontend Builds 2: readable and pluggable gulpfiles

See all articles from the series

The idea

Within this post I'll explain the steps I've used to move from the initial gulpfile to a readable and maintainable version of it. But before explaining the technical steps, let's talk about the target. Over the past year I've seen so many different styles and approaches for writing and organising gulpfiles, but most of them share the same problem. Once you revisit the project and it's gulpfile after a few weeks or months, you need a lot of time for reading and understanding what gulp is actually doing in this particular project when executing gulp default or just gulp. In order to reduce this effort, I've outlined my idea of how to align tasks and how things should interact using a small mockup

Gulp Build Architecture

Let me explain all requirements or properties of the main gulpfile.js at this point.

Default tasks

We're developers, so our job is building software, that's why default gulp tasks should automate all the things you need frequently. For me it's building frontend apps and starting a watcher which will automatically trigger the entire build as soon as I change a single bit inside of my development folder (usually named src).

Load / init gulptask files automatically

Nothing is more frustrating that a semi-automated system. One of the key requirements for a good gulpfile is the posibility to dynamically load gulptask-files for a well-known folder (gulptasks in our case). Think about CoC (Convention over Configuration) for your builds. Our gulpfile.js have to load and initialized all potentional gulptask-files from the gulptasks subfolder without requesting any user interaction.

The help task

Documentation is essential! I don't wanna mess 20 minutes browsing a huge or ten small gulpfiles in order to find all the gulp tasks fitting my needs. I want to execute gulp help and it should print all public tasks. Public tasks?! Let me explain that term quickly

Encapsulating things is essential in our business. But nobody cares about build tasks. Why are people not separating internal (or as I call them private tasks) from those gulptasks that can be called (or should be called) from anyone?

Implementing default tasks

First let's take care about the default task. When browsing the web for regular watch implementations you'll find a lot of guides that watch on changes and build once a file has changed. That's cool, but for me it's important to have an up-front build followed by the watch. Let's refactor the existing gulpfile to achieve our requirements

gulp.task('default', function(done) {  
    inSequence('private:build', function() {
        return gulp.watch('src/**/*', ['private:build']);
    });
});

inSequence is just an instance of the run-sequence node module, which is responsible to execute the up-front build before it starts watching for changes in my src sub directory.

Refactoring load mechanism of 3rd party modules

Before we can continue with either building dedicated gulp-task-files we've to refactor how 3rd party modules were loaded. The current state looks like this

var gulp = require('gulp'),  
    del = require('del'),
    concat = require('gulp-concat'),
    inject = require('gulp-inject'),
    cssmin = require('gulp-cssmin'),
    ngAnnotate = require('gulp-ng-annotate'),
    ngTemplateCache = require('gulp-angular-templatecache'),
    rename = require('gulp-rename'),
    shelljs = require('shelljs'),
    uglify = require('gulp-uglify'),
    inSequence = require('run-sequence');

But we want to pass all required 3rd party modules to those dedicated gulp-task-files so we've to bundle all those tasks like shown below

var gulp = require('gulp');

var tasks = {  
    del: require('del'),
    concat: require('gulp-concat'),
    inject: require('gulp-inject'),
    cssmin: require('gulp-cssmin'),
    ngAnnotate: require('gulp-ng-annotate'),
    ngTemplateCache: require('gulp-angular-templatecache'),
    rename: require('gulp-rename'),
    shelljs: require('shelljs'),
    uglify: require('gulp-uglify'),
    inSequence: require('run-sequence')
};

Building gulp-task-files

In order to have dedicated gulp-task-files which are responsible for certain scenarios like

  • desktop
  • web
  • mobile

and a fourth one which is responsible for common tasks, we need to define something like a contract. Because gulp is based on Node.JS we can easily utilise the module approach to achieve this.

Each gulp-task-file have to provide it's own documentation and of course it has to register a set of new gulp tasks. I ended up with defining a contract like this.

(function(module) {
    'use strict';

    function RegisterTasks(gulp, tasks) {

        gulp.task('build:matrix', function(done) {
          console.log("just a demo");
        });
    }

    module.exports = {
        init: RegisterTasks,
        docs: [{
            task: 'build:matrix',
            description: 'just to demonstrate'
        }]
    };

})(module);

Go and check the latest version of x-note in the repo, you can find the gulp-task-files in the subfolder gulptasks.

Dynamically loading all gulp-task-files

Before we can care about printing all the docs provided by our gulp-task-files from within the help task, we've to build a dynamic loader system. We can easily utilise require-dir to build such a dynamic loader.

The main gulpfile has to be refactored like this.

var customGulpTasks = require('require-dir')('./gulptasks');

for (var gulpTask in customGulpTasks) {  
    customGulpTasks[gulpTask].init(gulp, tasks);
}

Important is the last line of code here, where we call init on any found gulp-task-file and pass in gulp and all loaded submodules which are properties of the tasks property.

Building gulp help

Finally let's build the gulp help task, wich will iterate over all gulp-task-files and print their documentation. So the developer will directly see which gulp tasks he/she can call and what they're actually doing.

gulp.task('help', function() {  
    console.log('Execute one of the following commands\n');
    for (var gulpTask in customGulpTasks) {
        if (!customGulpTasks[gulpTask].hasOwnProperty('docs')) {
            continue;
        }
        customGulpTasks[gulpTask].docs.map(function(doc) {
            console.log("gulp " + doc.task + " - (" + doc.description + ")");
        });
    }
    console.log('\n');
});

Comments

comments powered by Disqus