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.
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.
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
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.