Dynamic Grunt Targets Using Templates

Grunt tasks are very handy when it comes to frontend development, tooling some Node.js routines or even publishing your fresh new hipster blog.

Dynamic file object maps various src to dest within one target.

But what about writing multiple targets at once — without add any line of code?

§tl;dr

Some Grunt tasks may last too long and you don’t want to keep adding targets in your Gruntfile.js. Here is a trick to expand targets automatically.

§The Initial Context

Mark McDonnell worked hard on making BBC News Sass compilation dynamic with Grunt.
He faced the problem of long Sass compilation time and wanted to split them in smaller chunks to reduce the time people stall during two changesets.

Hence the following file structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
├── sass
│   ├── afrique
│   ├── arabic
│   ├── hausa
│   ├── news
│   └── partials
│   ├── mixins
│   └── shared
└── stylesheets
├── afrique
├── arabic
├── hausa
└── news

§Writing Targets Manually

If we wanted to be able to rebuild Sass files individually for each service, we would have to write the following code in our Gruntfile.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module.exports = function (grunt) {
grunt.initConfig({
sass: {
afrique: {
expand: true,
cwd: 'src/sass/',
src: ['services/afrique/**/*.scss', '!**/_*.scss', '!partials/**/*.scss'],
dest: 'stylesheets/',
ext: '.css'
},
arabic: {
expand: true,
cwd: 'src/sass/',
src: ['services/arabic/**/*.scss', '!**/_*.scss', '!partials/**/*.scss'],
dest: 'stylesheets/',
ext: '.css'
},
// … repeat that countless times

// a single target to rule them all
dist: {
expand: true,
cwd: 'src/sass/',
src: ['**/*.scss', '!**/_*.scss', '!partials/**/*.scss'],
dest: 'stylesheets/',
ext: '.css',
options: {
style: "compressed"
}
}
}
});

grunt.loadNpmTasks('grunt-contrib-sass');
};

This way, you type grunt sass:afrique to compile only the BBC Afrique service stylesheets or grunt sass:dist to rebuild all the services stylesheets — typically done only when releasing otherwise you will feel like living this comic strip.

You hence face 2 problems:

  • the maintenance cost increases gradually as soon as new services requires to add new targets;
  • the readability decreases as your Gruntfile get more and more bloated by repetitive content.

Mark’s technique has been great at removing this pain — and he even improved it to leverage the Grunt Config API): he dynamically generated the Grunt targets on runtime.

I’ve been working at making BBC News Grunt tooling battle-tested and pluggable into their CI process. It gave me the opportunity to simplify things.

Because I had time and found it challenging.

§Enters Grunt Property Expand

The grunt.template mechanism is recommended to avoid repetitions, and to reuse configuration values.

Templates are evaluated on runtime, when a task is duely queued and ran. Not when grunt.initConfig is called.
Which means we have access to the grunt.task.current object.

In the case of grunt sass:afrique, grunt.task.current.name equals sass and then, its arguments: grunt.task.current.args is an array for which the first index equals afrique.

We don’t need to know more, we can define a static target and provide a complementary argument which will route the services properly like this:

1
2
3
grunt sass:service:afrique
grunt sass:service:arabic

Our Gruntfile.js will never grow in size or require any new line of code to target a sub-tree of our codebase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module.exports = function (grunt) {
grunt.initConfig({
sass: {
// called with grunt sass:service:afrique etc.
service: {
expand: true,
cwd: 'src/sass/',
src: ['services/<%= grunt.task.current.args[0] %>/**/*.scss', '!**/_*.scss', '!partials/**/*.scss'],
dest: 'stylesheets/',
ext: '.css'
},

// a single target to rule them all
dist: {
expand: true,
cwd: 'src/sass/',
src: ['**/*.scss', '!**/_*.scss', '!partials/**/*.scss'],
dest: 'stylesheets/',
ext: '.css',
options: {
style: "compressed"
}
}
}
});

grunt.loadNpmTasks('grunt-contrib-sass');
};

§The Bay Watcher

We can apply the same sugar to the watch task to recompile automatically our Sass files.
Still, we don’t want to rebuild the whole files assets. We only want to recompile service’s modified Sass files.

This is doable by applying the same technique, not only in the src target configuration, but also in the tasks’s one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = function (grunt) {
grunt.initConfig({
//…
watch: {
// grunt watch:service:afrique
// grunt watch:service:news
// …
service:{
files: [
'src/sass/partials/**/*.scss',
'src/sass/services/<%= grunt.task.current.args[1] %>/*.scss'
],
tasks: ['sass:service:<%= grunt.task.current.args[1] %>']
}
}
//…
});

grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-contrib-watch');
};

§Final Words

While writing this blogpost, I discovered a sentence in the Inside Grunt Tasks page documentation:

While a task is running, Grunt exposes many task-specific utility properties and methods inside the task function via the this object. This same object is also exposed as grunt.task.current for use in templates.

Some would say RTFM.
I would say it was worth finding and learning it.