Grunt Tricks: Part Two

June 20th 2014

Dynamic Alias Tasks

Commonly with Grunt, you'll have a block of config along with a block of task definitions:

// Config block
grunt.initConfig({
  clean: {
    dist: 'dist/',
  },
  builder: {
    app: { src: ['src/**/*.js'], dest: 'dist/app.js' },
    test: { src: ['test/**/*.js'], dest: 'dist/test.js' },
  },
});

// Tasks block
grunt.registerTask('build', ['clean', 'builder']);
grunt.loadNpmTasks('grunt-imaginary-builder');

Now any time you run grunt build it will clean the dist/ folder and run our imaginary builder task to compile your src/ and test/ files.

This is fine until your src and test become large and slow down your build. If we want to selectively build either src or test, we could use targets and additional tasks but that clutters up the Gruntfile. Let's turn build into a dynamic alias task:

grunt.registerTask('build', function(which) {
  var task = 'builder';
  if (which) task += ':' + which;
  grunt.task.run(['clean', task]);
});

grunt build will operate as before but now you have the option to grunt build:app will only run builder on our src/ files.

Arguments are passed to Grunt tasks separated by a colon, i.e.: grunt task:arg1:arg2.

Utilizing a function rather than an array for defining your task runs can help give you more programmatic control over your task flow.

Asynchronous Flow

Grunt is synchronous by design to make it easier for new users to approach. Each task will only run after the last task has finished, in a series. The Gruntfile loads synchronously and all of the APIs within Grunt run synchronously.

This is great for a new user becoming acclimated with JavaScript and works for most use cases. Although and quite often with JavaScript, you need to deal with asynchronous operations.

Async Dynamic Alias Task

Extending the dynamic alias task, what if we wanted to programmatically control the flow of our tasks but required an asynchronous operation to do so? Such as including the SHA of our git controlled project in the destination file name. We can update our build task as such:

grunt.registerTask('build', function(which) {
  // Instruct this task to wait until we call the done() method to continue
  var done = this.async();

  // Run `git rev-parse HEAD` to get the SHA
  grunt.util.spawn({
    cmd: 'git',
    args: ['rev-parse', 'HEAD']
  }, function(err, sha, stderr) {
    // TODO: Handle error

    // Alter the config to include the SHA in the destination file names
    grunt.config('builder.app.dest', 'dist/app.' + sha + '.js');
    grunt.config('builder.test.dest', 'dist/test.' + sha + '.js');

    // Schedule tasks to run immediately after this one
    var task = 'builder';
    if (which) task += ':' + which;
    grunt.task.run(['clean', task]);

    // All done, continue to the next tasks
    done();
  });
});

Read more about grunt.util.spawn

Now running grunt build will shell out and call git rev-parse HEAD to get the latest SHA then dynamically set the config for each destination file name to include it. Our imaginary builder task will compile files with destinations like such: dist/app.f56d40ee0f06f8c9b7ea53a7daec6a4d478356f9.js.

With this you can programmatically set your config and schedule the desired task flow.

Task All The Things

Nearly every question that begins with, "With Grunt, how do I..." The answer is: create a task.

I recommend creating tasks liberally. They truly are the best way to solve things with Grunt.

In doing so, you may find your Gruntfile growing long. This is where grunt.loadTasks becomes useful.

Create a folder named tasks/ and then add the line: grunt.loadTasks('tasks'); to your Gruntfile. Now any file put in the tasks/ folder will be loaded by Grunt.

Add a task by creating a file named tasks/build.js with the content of your task wrapped in a function:

// tasks/build.js
module.exports = function(grunt) {
  grunt.registerTask('build', function(which) {
    /* Contents of the task... */
  });
};

You can even move the portions of your config related to this task into this file by using grunt.config:

// tasks/build.js
module.exports = function(grunt) {

  // Sets only the "builder" section of your config
  grunt.config('builder', {
    app: { src: ['src/**/*.js'], dest: 'dist/app.js' },
    test: { src: ['test/**/*.js'], dest: 'dist/test.js' },
  });

  // Create a wrapper task to customize the task flow
  grunt.registerTask('build', function(which) {
    /* Contents of the task... */
    grunt.task.run(['builder']);
  });

  // Even load npm-installed plugins from here
  grunt.loadNpmTasks('grunt-imaginary-builder');
};

This keeps your main Gruntfile simple:

// Gruntfile.js
module.exports = function(grunt) {
  // Your initial config block
  grunt.initConfig({
    clean: {
      dist: 'dist/',
    },
  });
  // Load your app specific tasks
  grunt.loadTasks('tasks');
  // Load npm installed tasks
  grunt.loadNpmTasks('grunt-contrib-clean');
  // Your entry point task flows
  grunt.registerTask('default', ['build']);
};

Conclusion

Grunt defaults to declarative and config based but allows you to move as far as you want towards a imperative and programmatic build system.