The Antipattern

Random, sometimes useful information.

You don’t design monolithic apps. Why should your build process be any different? I was recently faced with a Gruntfile that was getting too large for it’s own good. Here’s one approach to break it down into smaller pieces.

Typical Structure

Your project might looks something like this:

+-Gruntfile.js
|
+-module_1
|
+-module_2
.
.
.
+-module_N

As you increase in complexity, your Gruntfile starts getting larger and harder to read. Let’s face it; it might’ve started out simple (maybe just a linter), but after you’ve added template compilation, CSS pre-processing, custom scripts and more, it’s getting hard to manage and troubleshoot.

In particular, your initConfig() section might be a sprawling mess.

Shared configuration

Starting with Grunt@0.4.6 you can now use config.merge() (Documentation) inside a gruntfile, to incrementally set-up the configuration for your build.

To illustrate with an overly simple example, let’s take this project as a starting point:

+-Gruntfile.js
|
+-client+
|       +-home.js
|
+-server+
        +-app.js

It’s got two modules, and let’s say that our build process only cares about linting the files.

The gruntfile might look something like this:

module.exports = function(grunt){
    grunt.initConfig({
        jshint:{
            files:['client/home.js','server/app.js']
        }
    });
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.registerTask('lint', ['jshint']);
    grunt.registerTask('build', ['lint']);
}

In order to break down this build, we’re going to add a new Gruntfile to each module

+-Gruntfile.js
|
+-client+
|       +-home.js
|       +-Gruntfile.js
|
+-server+
        +-app.js
        +-Gruntfile.js

Each of these gruntfiles is going to add only the parts it cares about to the config by using config.merge(), in addition to using targets (eg: jshint:client) to further segregate what gets built.

This would be one of the sub-gruntfiles:

/* client/Gruntfile.js */

module.exports = function(grunt){
    grunt.config.merge({
        jshint:{
            client:{
                src:[__dirname+'/home.js']
            }
        }
    });

    /*optional*/
    grunt.registerTask('lint:client', ['jshint:client']);
    grunt.registerTask('build:client', ['lint:client']);
    /* optional */
}

And this would be the new main gruntfile

/* /Gruntfile.js */
module.exports = function(grunt){

    //Load downstream Gruntfiles
    require('./client/Gruntfile')(grunt);
    require('./server/Gruntfile')(grunt);

    //Set up global tasks
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.registerTask('lint', ['jshint']);
    grunt.registerTask('build', ['lint']);
}

As you can see, the main gruntfile acts as an index for all the submodules, passing the grunt object to each one so the build can be incrementally configured. (Although it may very well be set up to run some tasks globally for the project).

To invoke it, one would simply do

$ grunt build

This will invoke all targets for build

Building submodules

Remember the optional block up there? If you want to build individual components, you would simply pass that target as a parameter to grunt:

$ grunt build:client

This will only invoke the build target client, which is defined in one of the downstream files.

Live example

https://github.com/lmarkus/Example_ModularGrunt

Comments