Skip to main content

Combining Tasks with Grunt

Steven Merrill | Director, DevOps

May 27, 2014


I was recently asked to help out with a few build steps for a Drupal project using Grunt as its build system. The project's Gruntfile.js has a drush:make task that utilizes the grunt-drush package to run Drush make. This task in included in a file under the tasks directory in the main repository.

tasks/drush.js
module.exports = function(grunt) {
  /**
   * Define "drush" tasks.
   *
   * grunt drush:make
   *   Builds the Drush make file to the build/html directory.
   */
  grunt.loadNpmTasks('grunt-drush');
  grunt.config('drush', {
    make: {
      args: ['make', '<%= config.srcPaths.make %>'],
      dest: '<%= config.buildPaths.html %>'
    }
  });
};

You can see that the task contains a few instances of variable interpolation, such as <%= config.srcPaths.make %>. By convention, the values of these variables go in a file called Gruntconfig.json and are set using the grunt.initConfigmethod. In addition, the configuration for the default task lives in a file called Gruntfile.js. I have put trimmed examples of each below.

Gruntfile.js
module.exports = function(grunt) {
  // Initialize global configuration variables.
  var config = grunt.file.readJSON('Gruntconfig.json');
  grunt.initConfig({
    config: config
  });
  // Load all included tasks.
  grunt.loadTasks(__dirname + '/tasks');
  // Define the default task to fully build and configure the project.
  var tasksDefault = [
    'clean:default',
    'mkdir:init',
    'drush:make'
  ];
  grunt.registerTask('default', tasksDefault);
};[/php]
Gruntconfig.json
[php]{
  "srcPaths": {
    "make": "src/project.make"
  },
  "buildPaths": {
    "build": "build",
    "html": "build/html"
  }
}

As you can see, the project's Gruntfile.js also has a clean:default task to remove the built site and a mkdir:inittask to make the build/html directory, and the three tasks are combined with grunt.registerTask to make the default task which will be run when you invoke grunt with no arguments.

A Small Change

In Phase2's build setup using Phing we have a task that will run drush make when the Makefile's modified time is newer than the built site. This allows a user to invoke the build tool and only spend the time doing a drush make if the Makefile has indeed changed. The setup needed to do this in Phing is configured in XML: if an index.php file exists and it is newer than the Makefile, don't run drush make. Otherwise, delete the built site and run drush make. The necessary configuration to do this in a Phing build.xml is below.

build.xml

You'll note that Phing also uses variable interpolation. The syntax, ${html}, is similar to regular PHP string interpolation. By convention, parameters for a Phing build live in a build.properties file.

A Newer Grunt

The grunt-newer plugin appears to be the proper way to handle this. It creates a new task prefixed with newer: to any other defined tasks. If your task has a src and dest parameter, it will check that src is newer than dest before running the task. In my first quick testing, I added a spurious src parameter to the drush:make task and then invoked the newer:drush:make task.

grunt.config('drush', {
  make: {
    args: ['make', '<%= config.srcPaths.make %>'],
    src: '<%= config.srcPaths.make %>',
    dest: '<%= config.buildPaths.html %>'
  }
});

That modification worked properly in concert with grunt-newer (and the drush task from grunt-drush task didn't complain about the extra src parameter,) but I still also needed to conditionally run the clean:default and mkdir:init only if the Makefile was newer than the built site.

Synchronized Grunting

The answer turned out to be to create a composite task using grunt.registerTask and grunt.task.run that combined the three tasks existing tasks and then use the grunt-newerversion of that task. The solution looked much like the following.

tasks/drushmake.js
module.exports = function(grunt) {
  /**
   * Define "drushmake" tasks.
   *
   * grunt drushmake
   *   Remove the existing site directory, make it again, and run Drush make.
   */
  grunt.registerTask('drushmake', 'Erase the site and run Drush make.', function() {
    grunt.task.run('clean:default', 'mkdir:init', 'drush:make');
  });
  grunt.config('drushmake', {
    default : {
      // Add src and dest attributes for grunt-newer.
      src: '<%= config.srcPaths.make %>',
      dest: '<%= config.buildPaths.html %>'
    }
  });
}

I could then invoke newer:drushmake:default in my Gruntfile.js and only delete and rebuild the site when there were changes to the Makefile.


Recommended Next
Development
A Developer's Guide For Contributing To Drupal
Black pixels on a grey background
Development
3 Steps to a Smooth Salesforce Integration
Black pixels on a grey background
Development
Drupal 8 End of Life: What You Need To Know
Woman working on a laptop
Jump back to top