TL;DR

Take a look at this repo. It outlines a simple approach that drastically improves the organizational structure of a project's Grunt setup. It removes the need for a single, monolithic Gruntfile and breaks tasks down into their own files.

If you don't know what Grunt is...

If you're not familiar with Grunt, I encourage you to check it out. It's well worth your time. Instead of describing its benefits in my own words, I'll let Grunt speak for itself:

"In one word: automation. The less work you have to do when performing repetitive tasks like minification, compilation, unit testing, linting, etc, the easier your job becomes. After you've configured it, a task runner can do most of that mundane work for you—and your team—with basically zero effort."

Grunt is billed as a "JavaScript task runner" due to the fact that the underlying library and its large ecosystem of plugins run on top of Node.js. Take note, though - even if a project doesn't rely heavily on JavaScript for its own architecture, Grunt can still serve its purpose as a terrific task runner. To derive the most benefit, however, you'll want to be comfortable with JavaScript (and Node in particular) in order to setup tasks to your liking.

For the rest of you...

Grunt's slick, right? But for projects of even moderate complexity, the default organizational structure (one giant configuration file - Gruntfile.js) quickly becomes unwieldy. I've never cared for that aspect of it, but I've recently discovered a much better approach.

Copy the contents of your project's existing "Gruntfile" to a temporary location - you'll want to reference it later. Next, replace it entirely with this one. This version tells Grunt to look for tasks not within our Gruntfile, but instead within a "/tasks" folder that lives at the root of your project (you'll need to create that, by the way). You'll also need to install a couple of plugins ("load-grunt-tasks", "glob") by running:

$ npm install load-grunt-tasks glob --save

With this change in place, let's take a look at how you would go about defining a basic task. For this example, we'll use "build" as our task name.

Directory Structure

./Gruntfile.js ./tasks/build.js

The Contents of build.js

module.exports = function(grunt) { grunt.registerTask('build', function() { // Wondrous events occur here. }); };

From your terminal, you should now be able to run:

$ grunt build

You won't see much, because we haven't actually done anything within our task. For more on that subject, I suggest you reference the Grunt documentation.

Step Two: Create Grunt.config.js

In addition to the Gruntfile that lives within our project's root folder, you'll also want to create a file named Grunt.config.js . An example of this file's contents can be found below, which you can also download here.

module.exports = function(grunt) { return { 'alias': { 'something': 'something_else', 'derp': ['herp1', 'herp2'] } }; };

The purpose of this file is to provide you with a place where you can continue to define global Grunt configuration settings. Our updated Gruntfile also allows us to define task aliases here. For example, with this file in place, a call to the derp task will result in the herp1 and herp2 tasks being called (in that order). For the most part, though, this file will probably see little usage, after you complete the third (and final) step.

Step Three: Defining Task Options

In a previous example, we defined a "build" task at the following location:

./tasks/build.js

What we didn't do, though, was define any configuration settings for this task. This can now be done by creating a corresponding file at the following location:

./tasks/options/build.js

The contents of this file can be found below:

/** * These settings will be loaded into Grunt's configuration under the `build` * key: grunt.config.build */ module.exports = { 'key': 'value' };

In Conclusion

The ultimate goal that we're aiming for here is the avoidance of a single, monolithic Gruntfile in which every conceivable task and option related to a project can be found. We'd never write our code that way - why treat our task runner any differently?