Grunt is a very powerful task runner and if you are developing in Javascript chances are you are probably using it. It’s configuration file defines all the tasks available and what they should do, but have you ever felt that even for mid-sized projects it easily becomes too big and hard to manage?

Well I felt that way, so that’s when I decided to split it into many smaller configuration files with the help of load-grunt-config. By following the excellent advice given by Paul Bakaus on his article on html5rocks, I came up with a cleaner and much more manageable setup.

Here I’ll try to build upon his article to add some more details about useful tricks I used in my setup.

The brevity of YAML

Being at heart a Python developer, I love terse syntax. Why bother with unecessary brackets, commas or quotes which clutter your files making them harder to read for the human eye? Hence a move from a Javascript syntax to a YAML syntax for a configuration file is a big relief for me!

load-grunt-config supports many different kinds of syntax files, which can be mixed, allowing me to greatly reduce the noise in my configuration. If I initially had a Gruntfile like this:

module.exports = function (grunt) {
  var pkg = grunt.file.readJSON('package.json');

  grunt.initConfig({
    ngconstant: {
      options: {
        name: 'config',
        dest: 'app/config/config.js',
        constants: {
          ENV: {
            apiLocation: '',
            appName: 'MyApp',
            appVersion: pkg.version
          }
        }
      },
      development: {
        constants: {
          ENV: {
            apiLocation: 'http://localhost:3000/api'
          }
        }
      },
      staging: {
        constants: {
          ENV: {
            apiLocation: 'https://staging.example.com/api'
          }
        }
      },
      production: {
        constants: {
          ENV: {
            apiLocation: 'https://api.example.com'
          }
        }
      }
    }
  });
}

I would now have this as a Gruntfile:

module.exports = function (grunt) {
  require('load-grunt-config')(grunt);
};

And I would create a YAML configuration file in grunt/ngconstant.yaml:

options:
  name: 'config'
  dest: 'app/config/config.js'
  constants:
    ENV:
      apiLocation: ''
      appID: 'MyApp'
      appVersion: '<%= package.version %>'
  development:
    constants:
      ENV:
        apiLocation: 'http://localhost:3000/api'
  staging:
    constants:
      ENV:
        apiLocation: 'https://staging.example.com/api'
  production:
    constants:
      ENV:
        apiLocation: 'https://api.example.com'

Isn’t this cleaner? And it’s shorter, too!

Also notice how I did not have to manually parse my package.json to access its values? This is another nice feature of load-grunt-config’s which comes in handy. All you need to do is to read your values using a grunt template like '<%=package.version%>'.

Sharing values between tasks

In my project I used to have some variables holding some values shared by many different tasks. Lets take for instance the files list, which can be used by concat, but also by uglify or copy or whatever. So I had something like this:

module.exports = function (grunt) {

  var files = [

    // Application files
    'app/**/*.module.js',
    'app/module/**/*.js',
    'app/component/**/*.js',
    'app/service/**/*.js',
    'app/app.js',

    // Do not include tests in application build
    '!app/**/*.test.js',
    '!app/**/*_test.js'
  ];

  grunt.initConfig({
    jshint: {
      options: {
        jshintrc: true
      },
      all: {
        src: [files]
      }
    },
    uglify: {
      production: {
        src: files,
        dest: 'dist/app/js/<%= package.name %>.min.js',
        sourceMap: true
      }
    }
  });
};

How can I do to share these values across tasks now that they each live in their own file? Well first I created a grunt/project.yaml file holding these values:

files:
  # Application files
  - 'app/**/*.module.js'
  - 'app/module/**/*.js'
  - 'app/component/**/*.js'
  - 'app/service/**/*.js'
  - 'app/app.js'
    # Do not include tests in application build
  - '!app/**/*.test.js'
  - '!app/**/*_test.js'

And here’s the magic in the grunt/jshint.yaml file:

options:
  jshintrc: true
all:
  src:
    - '<%= project.files %>'

And the same goes for grunt/uglify.yaml:

production:
  src: '<%= project.files %>'
  dest: 'dist/app/js/<%= package.name %>.min.js'
  sourceMap: true

That was easy!

Separating configuration files by environment

As you could see, in my project I use grunt-ng-constant to create an appropriate configuration file according to the environment. I also use grunt-env in combination with grunt-preprocess to configure my index.html file differently according to the environment. I wanted to have a single file with all the configuration for a single environment so that it would be easier to verify and change them, and once again load-grunt-config came to the rescue!

I used config grouping to do that:

# File: development-tasks.yaml
ngconstant:
  constants:
    ENV:
      appVersion: '<%= package.version %>-dev'
    apiLocation: 'https://devel.example.com/api'
env:
  ENV: 'DEVELOPMENT'
  THEME_CSS: 'css/mytheme.devel.css'

Using this configuration I can now have a separate file for each environment. The grunt/ngconstant.yaml and grunt/env.yaml files become like this:

# File: grunt/ngconstant.yaml
options:
  name: 'config'
  dest: 'app/config/config.js'
  constants:
    # These are all default values
    ENV:
      appName: 'MyApp'
      appVersion: '<%= package.version %>'
    apiLocation: 'https://api.example.com/api/v1'
# File: grunt/env.yaml
options:
  add:
    THEME_CSS: 'css/mytheme.css'

As you can see, there are only the default options there and no environment specific configuration.

I also wanted to separate the environment-specific configuration files from the standard task configurations, so that the context would be clear. So I put them all in a config directory, which I created, and modified our Gruntfile.js:

module.exports = function (grunt) {

  var path = require('path');

  require('load-grunt-config')(grunt, {
    configPath: [
      path.join(process.cwd(), 'grunt'), // Task settings here
      path.join(process.cwd(), 'config') // Environment specific settings here
    ],

    /*
     * If you want to change your setting locally without changing what is on
     * the repo, you can define your configuration overrides in config/override
     */
    overridePath: [
      path.join(process.cwd(), 'config', 'override')
    ],

  });

};

load-grunt-config also supports overrides, which is another nice feature: as I set it up, you can create a configuration file in config/override which will override any previously set configuration option. This is very useful to accomodate the needs of every developer which might be a little different, without having them modify the files which are under versioning. Pretty neat!

Conclusion

load-grunt-config is a very nice grunt plugin which really improves the quality of your Gruntfile by splitting it into many smaller files. Not only that: it adds a lot of useful features that will make you wonder why bother writing plain Gruntfiles anymore!

Do you use load-grunt-config in your project? Is there some other great feature I missed? Please leave a comment below!