The idea of breaking up your code into smaller bite sized chunks creates an environment that is easy to work in and maintain. That’s often thought of as module design, and is a standard for web development these days. I’m going to show you a way you can use module design to better organize your Grunt tasks.
I’m going to assume that you already know the basics of using Grunt. If you don’t, here’s an article by Chris to get you going: Grunt for People Who Think Things Like Grunt are Weird and Hard.
If you use Grunt, you’re probably used to seeing your Gruntfile looking something like this.
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
sass: {
dist: {
options: {
style: 'expanded',
sourcemap: 'none'
},
files: {
'style.css': 'sass/global.scss',
'css/dev.style.css': 'sass/global.scss',
'css/ie9.style.css': 'sass/ie9.scss',
}
}
},
postcss: {
options: {
processors: [
require('autoprefixer')(),
require('rucksack-css')({ fallbacks: true })
]
},
dist: {
src: 'style.css',
dest: 'style.css'
},
dev: {
src: 'css/dev.style.css',
dest: 'css/dev.style.css'
},
},
cssmin: {
target: {
files: {
'style.css': 'style.css'
}
}
},
concat: {
dist: {
src: [
'js/lib/no-conflict.js',
'js/lib/skip-navigation.js',
],
dest: 'js/scripts.js'
},
},
jshint: {
files: [
'js/scripts.js',
'js/ie.js',
],
options: {
scripturl: true,
globals: {
jQuery: true
}
}
},
uglify: {
options: {
mangle: false,
compress: true,
quoteStyle: 3
},
dist: {
files: {
'js/head.min.js': 'js/head.js',
'js/scripts.min.js': 'js/scripts.js',
'js/ie.min.js' : 'js/ie.js',
}
}
},
watch: {
scripts: {
files: ['js/**/*.js'],
tasks: ['concat', 'uglify'],
options: {
spawn: false
}
},
css: {
files: ['sass/**/*.scss'],
tasks: ['sass', 'postcss', 'cssmin']
}
},
});
grunt.loadNpmTasks('grunt-postcss');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-jsvalidate');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.registerTask('default', ['watch']);
};
This is pretty normal looking Gruntfile. Actually this is a pretty small one. I’ve seen Gruntfiles that have been three time this size. Looking at those large Gruntfiles gives me a headache. I’ve had module design drilled into me so much that seeing a Gruntfile that large throws me off my game. Why can’t my Gruntfile look more like this?
module.exports = function(grunt) {
var tasks = {scope: ['devDependencies', 'dependencies' ]};
var options = {config: { src: "grunt/*.js" }};
var configs = require('load-grunt-configs')(grunt, options);
require('load-grunt-tasks')(grunt, tasks);
grunt.initConfig(configs);
grunt.registerTask('default', ['watch']);
};
It can! I’m going to show you how.
To accomplish this, we’re going to install two Grunt packages.
Go ahead, and install these packages as you would any other Grunt package.
$ npm install --save-dev load-grunt-tasks
$ npm install --save-dev load-grunt-configs
Now your `package.json` file should include the two packages like this.
{
"name": "your-project",
"version": "1.0.0",
"description": "",
"main": "Gruntfile.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"grunt-contrib-cssmin": "^1.0.1",
"load-grunt-configs": "^1.0.0",
"load-grunt-tasks": "^3.5.0"
}
}
For the sake of this article, I also included grunt-contrib-cssmin. Your `package.json` file should have any Grunt packages you’ll need for your project.
Now, let’s start with a fresh `Gruntfile.js`. Create a new `Gruntfile.js` file and add the following.
module.exports = function(grunt) {
}
The first thing we’ll do is set up load-grunt-tasks. What load-grunt-tasks does is it creates all of your grunt.loadNpmTasks()
for you. This removes the need to having to write out each grunt.loadNpmTasks()
by hand.
Let’s create a variable tasks
that will define the options for the load-grunt-tasks package. All we’re going to do is define the scope and tell it to create a grunt.loadNpmTasks()
for all the devDependencies
and dependencies
packages defined in the `package.json` file.
module.exports = function(grunt) {
var tasks = {scope: ['devDependencies', 'dependencies']};
}
We’ll need to require() it and add the tasks
variable to it as the second parameter. Let’s also add two more variables options and configs for load-grunt-configs.
module.exports = function(grunt) {
var tasks = {scope: ['devDependencies', 'dependencies' ]};
var options = {config: { src: "grunt/*.js" }};
var configs = require('load-grunt-configs')(grunt, options);
require('load-grunt-tasks')(grunt, tasks);
}
In the options variable we telling load-grunt-configs where to look for the files that will contain the grunt task options. The configs variable is simply requiring load-grunt-configs and adding the options as the second variable.
Finally, we’ll add the configs
variable to the grunt.initConfig()
function and register a basic task that will run grunt-contrib-cssmin.
module.exports = function(grunt) {
var tasks = {scope: ['devDependencies', 'dependencies' ]};
var options = {config: { src: "grunt/*.js" }};
var configs = require('load-grunt-configs')(grunt, options);
require('load-grunt-tasks')(grunt, tasks);
grunt.initConfig(configs);
grunt.registerTask('default', ['cssmin']);
}
That’s all we’ll be putting into the Gruntfile. The only other things you may want to add would be additional tasks you want to register. Like a watch task or a build task.
In the options
variable we defined the src as grunt/*.js
. This tells load-grunt-configs to look in a directory named grunt and include all JavaScript files within it.
Let’s create a directory named grunt and add the task options for cssmin by creating a file named `cssmin.js`. Your project’s directory structure should look similar to this now.

Let’s add the cssmin options to the `cssmin.js` file. The first thing you want to add is the Grunt module.exports variable just like in the Gruntfile, so that Grunt knows this file is to be loaded when you run your Grunt task. Then add the options for cssmin:
module.exports = {
target: {
files: {
'style.css': 'styles.css'
}
}
};
If you noticed I didn’t add the 'cssmin: {}'
wrapper like you would in a basic Gruntfile. This is because load-grunt-configs uses the file’s name to recognize which task is being run. For example if you’re using grunt-contrib-uglify the file name would be `uglify.js`. If you’re using grunt-postcss. That file name will be `postcss.js`.
This is how we’re adding the module design concept to our grunt tasks. Each task will have its own file containing the task’s options. This makes to easy to add new tasks, and for multiple developers to makes changes without the worries of accidentally messing up another task in a enormous Gruntfile.
Now when you run your grunt task. Grunt will look in the `grunt` folder find all the task files and run those tasks using the options you defined within each file.
This was a basic example of this technique. The load-grunt-tasks and load-grunt-configs packages have more options available that can give you even more control of your Grunt tasks.
I hope this helps you get your Gruntfile under control and add more flexibility to your projects using Grunt.
Does anyone still use grunt in non legacy projects?
Yes. https://github.com/LotusTM/Kotsu/tree/release/1.0.0
I still use it — I haven’t had a really compelling reason to switch to whatever’s hip these days (is it webpack? NPM scripts? Do we hate Gulp now? Can’t keep track anymore).
I’m sure there are plenty of reasons to swap to something else but Grunt does a fine job compiling scss/minifying stuff/whatevs. What are your main gripes with Grunt? Is it that there’s too much boilerplate or is there something wrong with it that actually impacts end users?
I still use it also. I’ve yet to find a compelling reason to change workflow across my team. I always keep an eye out though.
Relevant:
@Alex – The reason I personally switched from Grunt to Gulp was that I felt that Gulp was significantly faster at running tasks. It very well could have been a poorly written Gruntfile on my side – I didn’t run any real, hard tests on it.
Hmm, that’s fair. The only significant issue I’ve had performance-wise is compiling SCSS. For me, switching from
grunt-contrib-sass
(which uses Ruby Sass) togrunt-sass
(which uses LibSass) helped a lot.If I notice things slowing down too much maybe I’ll take that as a cue to move on to something else, thanks.
I was also using Ruby Sass, and when I switched I also jumped over to PostCSS, both of which for sure helped with performance issues as well.
Same here. I found grunt-contrib-sass to be extremely slow. Once I moved to postcss + grunt-sass things have sped up tremendously.
I use Grunt and Gulp together , because Grunt has way more packages available than Gulp does. You get the best of both worlds then.
Good post – large Grunt/Gulpfiles are certainly a pain to look at. I’ve taken to using the same approach in Gulp. My gulpfile now looks like:
With individual tasks in their own Javascript files within the ./gulp-tasks directory. Much more managable!
I thought that style require dir was not best practice and will cause heart burn when you upgrade to gulp 4
Thanks for the post, we’re big fans of splitting our tasks out like this where I work.
It can actually be simplified even more – you don’t need to install
load-grunt-tasks
asload-grunt-config
includes it.And the Gruntfile.js file can become as simple as:
load-grunt-config will by default look for a
grunt/
folder for your tasks, and you can still pass in options to load-grunt-tasks if you need to.Great post, thanks for sharing. Does anyone know the pros and cons of using
load-grunt-tasks
instead ofmatchdep
to automate task loading?require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
Great article, I’m actually interested in knowing if this would work with jit-grunt.
Yes, it absolutely works.
I highly recommend
jit-grun
overload-grunt-tasks
since it initializing tasks significantly faster.Besides, you don’t need
load-grunt-configs
to load task files, since Grunt already has loader for external configs —grunt.loadTasks
(docs).Here is example from our config file