Introducing grunt-horde, packageable grunt configuration modules

node.js grunt npm grunt-horde gitemplate packaging

Maintaining configurations

After building more and more node.js modules based on code generated by gitemplate, I ended up with a lot of nearly identical Gruntfile.js files. Scaffolding git repositories saved time upfront, but maintaining grunt configuration was time-consuming.

audit-shelljs assisted automated maintenance when possible, but the underlying problem was still duplicate Gruntfile.js content.

grunt-horde

File sanity

Instead of holding all config in a large Gruntfile.js and/or using require to split some logic chunks into one-offs, grunt-horde shrinks a Gruntfile.js to something like:

Each loot call accepts a directory path or local NPM package name, then recursively merges in the module’s key/value payload.

Packageable modules

Inside a module directory, file name conventions follow grunt method names, ex. initConfig/<plugin>.js and registerTask.js. This initConfig/jshint.js file adds a middleware property to the final jshint section applied to grunt.initConfig:

API

Each module.exports function discovered by loot receives grunt as an argument and also context methods for reading, writing and removing keys: this.learn, this.demand, this.kill. The methods are also available on the object produced by create in Gruntfile.js. And each supports deep object-path strings like jshint.lib.files.src.

Composition

loot allows developers to compose modules based on inheritance. The first module imported by loot could seed a jshint section with patterns very common in dependent projects, then more granular modules could update/remove them as needed. Several module intents:

All supported sections — initConfig/<plugin>.js, loadTasks, loadNpmTasks, registerTask, registerMultiTask — are similarly composable.

Events

To help debug compositions, grunt-horde emits several events on the grunt.event bus for tracing the source of config mutations.

In use

I use grunt-horde in conjure to load several modules and set some config keys manually.

  1. Imports a baseline node-component-grunt module from NPM.
  2. Imports node-lib-grunt and node-bin-grunt from NPM that add/merge/overwrite/remove values inherited from node-component-grunt.
  3. Loads project-specific overrides from ./config/grunt.
  4. Locks several initConfig keys to project-specific values. For example, grunt evaluates <%= projName %> as conjure even inside node-component-grunt.

Try it