Combining Tasks with Grunt
This post was originally posted on the Phase2 Blog.
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.initConfig
method. 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);
};
Gruntconfig.json #
{
"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:init
task 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 #
<target name="-drush-make-uptodate" depends="init" hidden="true">
<if>
<available file="${html}/index.php" />
<then>
<uptodate property="drush.makefile.uptodate"
targetfile="${html}/index.php" srcfile="${drush.makefile}" />
</then>
</if>
</target>
<!-- Use drush make to build (or rebuild) the docroot -->
<target name="drush-make" depends="-drush-make-uptodate, init"
hidden="true" unless="drush.makefile.uptodate">
<if>
<available file="${html}"/>
<then>
<echo level="info" message="Rebuilding ${html}."/>
<delete dir="${html}" failonerror="true"/>
</then>
</if>
<exec executable="drush" checkreturn="true" passthru="true" level="info">
<arg value="make"/>
<arg value="${drush.makefile}"/>
<arg value="${html}"/>
</exec>
</target>
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-newer
version 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.