Dorokhov.codes

03. Grunt

Grunt is a task runner written in JavaScript.

Let’s install it

npm install grunt --save-dev
touch Gruntfile.js

Let’s config it

The config file is called Gruntfile.js. It is used as a standard CommonJS module:

module.exports = function(grunt) {
  // Do grunt-related things in here
};

What about the API?

The grunt object is what we’ll use to interact with Grunt.

  • grunt.config - methods for updating and retrieving configuration.
  • grunt.task - methods for loading and registering tasks.
  • grunt.file - methods for reading and writing files.

Let’s create some basic configuration:

// Option 1:
grunt.initConfig({
  myTask: {
      firstName: "Andrew",
      secondName: "Dorokhov"
  }
});

// Option 2:
grunt.config.init({
  myTask: {
    firstName: "Andrew",
    secondName: "Dorokhov"
  }
});

// Option 3:
grunt.config.set('myTask.firstName', 'Andrew');
grunt.config.set('myTask.secondName', 'Dorokhov');

// Option 4:
grunt.config('myTask.firstName', 'Andrew');
grunt.config('myTask.secondName', 'Dorokhov');

grunt.config.init() (alias grunt.initConfig()) - set the configuration object. This call will erase all prior configuration.

How to receive a parameter?

grunt.config.get('myTask.firstName');
grunt.config('myTask.firstName');

Templates

grunt.initConfig({
  foo: 'c',
  bar: 'b<%= foo %>d',
  bazz: 'a<%= bar %>e'
});
grunt.registerTask('default', function() {
  grunt.log.writeln( grunt.config.get('bazz') ); // We will get "abcde"
});

When we use grunt.config.get(), internally Grunt is using the grunt.template.process() function to resolve each template recursively.

Another example:

grunt.initConfig({
  foo: ['a.js','b.js','c.js','d.js'],
  bazz: '<%= foo %>'
});

Creating tasks

A Grunt task is essentially just a JavaScript function, and that’s it.

Let’s create one:

//  It's an alias for `grunt.task.registerTask()`.
grunt.registerTask('foo', function() {
  grunt.log.writeln('foo is running...');
});

Run a task from another task:

grunt.task.run('jshint');

The task object

When tasks are executed, the current task object is used as the function context, where it may be accessed via the JavaScript this operator or the grunt.current.task object.

The task object has the following properties:

  • name: a string set to the task name (the first parameter provided to grunt.registerTask).
  • async: a function which notifies Grunt that this task is asynchronous and returns a callback.
  • requires: a function which accepts an array of task names (strings), then ensures that they have previously run. So, if we had a deploy task we might use this.requires(["compile"]), which will ensure we have compiled our code before we deploy it.
  • requiresConfig: an alias to the grunt.config.requires() function. This function causes the current task to fail if a given path configuration property does not exist.
  • nameArgs: a string set as the task name including arguments used when running the task.
  • args: an array of all arguments used to run the task.
  • flags: an object which uses each of the args as its keys and true as the value. This allows us to use the arguments as a series of switches. So, if we ran the task foo with grunt foo:one:two, then this.flags.two would be true but this.flags.three would be undefined (which is falsy).
  • errorCount: a number representing the number of calls to grunt.log.error().
  • options: a function used to retrieve the task’s configuration options which is functionally equivalent to grunt.config.get([this.name, "options"]).

A multitask object has some additional parameters:

  • target: a string set to the target name (the property name used inside our Grunt configuration).
  • files: an array of file objects. Each object will have an src property and an optional dest property.
  • filesSrc: an array of strings representing only the src property of each file object from the above files array.
  • data – which is the target object itself.

options in a multitask is a function used to retrieve the combination of the task’s and target’s configuration options. This is functionally equivalent to merging the results of grunt.config.get([this.name, "options"]) and grunt.config.get([this.name, this.target, "options"]).

This is useful because the user of the task can set task-wide defaults and then, within each target, they can override these defaults with a set of target-specific options.

Asynchronous tasks

Asynchronous tasks should be specified another way so take a look at the documentation.

Task arguments

A simple task:

module.exports = function(grunt) {
  grunt.registerTask('foo', function(p1, p2) {
    console.log('first parameter is: ' + p1);
    console.log('second parameter is: ' + p2);
  });
};
// Run: grunt foo:bar:bazz

A multitask:

module.exports = function(grunt) {
  grunt.initConfig({
    foo: {
      ping: {},
      pong: {}
    }
  });
  
  grunt.registerMultiTask('foo', function(p1, p2) {
    console.log('target is: ' + this.target);
    console.log('first parameter is: ' + p1);
    console.log('second parameter is: ' + p2);
  });
};
// Run: grunt foo:ping:bar:bazz

Runtime options

Runtime options are used to create Grunt-wide settings for a single execution of Grunt.

Runtime options must be prefixed with at least one dash, “-”, otherwise they will be seen as task name.

grunt foo --bar # will be true
grunt foo --bar=123 # will be 123
module.exports = function(grunt) {
  console.log('bar is: ' + grunt.option('bar'));
  grunt.registerTask('foo', function() {
  // ...
  });
};

Another example of how to use runtime options:

// Initialize environment
var env = grunt.option('env') || 'dev';
// Environment specific tasks
if(env === 'prod') {
  grunt.registerTask('scripts', ['coffee', 'uglify']);
  grunt.registerTask('styles', ['stylus', 'cssmin']);
  grunt.registerTask('views', ['jade', 'htmlmin']);
} else {
  grunt.registerTask('scripts', ['coffee']);
  grunt.registerTask('styles', ['stylus']);
  grunt.registerTask('views', ['jade']);
}
// Define the default task
grunt.registerTask('default', ['scripts','styles','views']);

Or:

grunt.registerTask('scripts', function() {
  grunt.task.run ('coffee');
  if(env === 'prod') {
    grunt.task.run('uglify');
  }
});

Task aliasing

Instead of providing a function to grunt.registerTask, we can also provide an array of strings; this will create a new task that will sequentially run each of the tasks listed in the array.

module.exports = function(grunt) {
    
  grunt.registerTask('build', function() {
    console.log('building...');
  });
  
  grunt.registerTask('test', function() {
    console.log('testing...');
  });
  
  grunt.registerTask('upload', function() {
    console.log('uploading...');
  });
  
  grunt.registerTask('deploy', ['build', 'test', 'upload']);
};

Task help

Using the grunt --help command we can see the task description. Here’s how to specify them:

module.exports = function(grunt) {
  grunt.registerTask('analyze',
    'Analyzes the source',
    function() {
        console.log('analyzing...');
    }
  );
  grunt.registerMultiTask('compile',
    'Compiles the source',
    function() {
        console.log('compiling...');
    }
  );
  grunt.registerTask('all',
    'Analyzes and compiles the source',
        ['analyze','compile']
  );
};

Smart tasks

We can check from the task if we have all the necessary parameters:

// fail if configuration is not provided
grunt.config.requires('myTask.firstName');
grunt.config.requires('myTask.secondName');
// fail and we will not be able to ignore the task using  the`--force` flag
grunt.fail.fatal('myTask.firstName');

Default options

When this.options() called inside a task, it looks for the task’s configuration by name and then looks for the options object.

grunt.initConfig({
    myTask: {
        options: {
            bar: 7
        },
        foo: 42
    }
});

grunt.registerTask('myTask', function() {
    this.options(); // { bar:7 }
});

This feature is most useful in multitasks as we are able to define a task-wide options object, which may be overridden by our target-specific options.

grunt.initConfig({
    myTask: {
        options: {
            foo: 42,
            bar: 7
        },
        target1: {
        },
        target2: {
            options: {
                bar: 8
            }
        }
    }
});

So options is a reserved word. Grunt will use each property (except options) of a multi task’s configuration as an individual configuration, called a target.

Working with files

Grunt has some functionality for multitasks to work with files. We can describe files and Grunt will place all matching files in the task’s files array (this.files within the context of a task).

this.files.forEach(function(file) {
    console.log("source: " + file.src + " -> " + "destination: " + file.dest);
});

Each file in the files array contains src and dest properties.

Every task target in a config can have the src property. Optional also a dest property.

Specifying the matching:

target1: {
    src: ['src/a.js', 'src/b.js']
}
target1: {
    src: 'src/{a,b,c}.js'
}

To describe multiple source sets with single destination, we can use the “Files array format”:

target1: {
  files: [
    { src: 'src/{a,b,c}.js', dest: 'dest/abc.js' },
    { src: 'src/{x,y,z}.js', dest: 'dest/xyz.js' }
  ]
}

We can get an equivalent result with the more compressed: “Files object format”:

target1: {
  files: {
    'dest/abc.js': 'src/{a,b,c}.js',
    'dest/xyz.js': 'src/{x,y,z}.js'
  }
}

Mapping a source directory to destination directory

Often we would like to convert a set of source files into the same set of destination files. In this case, we’re essentially choosing a source directory and a destination directory.

Intead of:

target1: {
  files: [
    {src: 'lib/a.js', dest: 'build/a.min.js'},
    {src: 'lib/b.js', dest: 'build/b.min.js'},
    {src: 'lib/subdir/c.js', dest: 'build/subdir/c.min.js'},
    {src: 'lib/subdir/d.js', dest: 'build/subdir/d.min.js'},
  ],
}

we can use:

target2: {
  files: [
  {
    expand: true,       // expand Set to true to enable the following options.
    cwd: 'lib/',        // `cwd`: All `src` matches are relative to (but don't include) this path.
    src: '**/*.js',     // `src`: Pattern(s) to match, relative to the `cwd`.
    dest: 'build/',     // `dest` Destination path prefix.
    ext: '.min.js'      // `ext` Replace any existing extension with this value in generated dest paths.
                        // `flatten`: Remove all path parts from generated dest paths.
                        // `rename`: This function is called for each matched `src` file, (after extension renaming
                        //  and flattening). The `dest` and matched `src` path are passed in, and this function
                        //  must return a new `dest` value. If the same `dest` is returned more than once, each
                        //  `src` which used it will be added to an array of sources for it."
    },
  ],
}

Executing tasks

Execute the task:

grunt foo

Multitasks

We use multitasks when we have the same task but want to use different configs.

This is a simple task config:

grunt.initConfig({
    myTask: {
        firstName: "Andrew",
        secondName: "Dorokhov"
    }
});
// To execute: `grunt myTask`

This is a multitask config:

grunt.initConfig({
    myTask: {
        andrew: {
            firstName: "Andrew",
            secondName: "Dorokhov"
        },
        john: {
            firstName: "John",
            secondName: "Smith"
        }
    }
});
// To execute: 
// `grunt myTask:andrew`
// `grunt myTask:john`

It’s called targets.

If we omit the target name and simply use the command, then Grunt will run all targets of the task.

Plugins

Also, we can load a task from some Grunt plugin (one plugin can contain several tasks):

// Load the plugin that provides the "jshint" task.
grunt.loadNpmTasks('grunt-contrib-jshint');

Configuration

Right order:

concat: {
  build: {
    src: [
      "src/scripts/**/*.js",
      "!src/scripts/app.js",
      "src/scripts/app.js"
    ],
    dest: "build/js/app.js"
  }
}

We must first exclude the line from the file set (by prefixing the file path with an exclamation mark !), then re-include it.

Targets

Targets allow us to define multiple configurations for a task.

We can run a task with a particular configuration (target):

grunt sometask:target1

Plugins I use

  • JSHint.
  • grunt-contrib-sass •Minify JavaScript—http://gswg.io#grunt-contrib-uglify •Minify CSS—http://gswg.io#grunt-contrib-cssmin •Minify HTML—http://gswg.io#grunt-contrib-htmlmin

grunt.initConfig({ uglify: { target1: { src: ‘foo.js’, dest: ‘foo.min.js’ } } });

Concat:

// Load the plugin that provides the "concat" task.
grunt.loadNpmTasks('grunt-contrib-concat');
// Project configuration.
grunt.initConfig({
    concat: {
        target1: {
            files: {
                "build/abc.js": ["src/a.js", "src/b.js", "src/c.js"]
            }
        }
    }
});

Useful approaches

Read a JSON-file into a variable:

grunt.initConfig({
    someOptions: grunt.file.readJSON('credentials.json')
});