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 togrunt.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 adeploy
task we might usethis.requires(["compile"])
, which will ensure we have compiled our code before we deploy it.requiresConfig
: an alias to thegrunt.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 withgrunt foo:one:two
, thenthis.flags.two
would betrue
butthis.flags.three
would beundefined
(which is falsy).errorCount
: a number representing the number of calls togrunt.log.error()
.options
: a function used to retrieve the task’s configuration options which is functionally equivalent togrunt.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 ansrc
property and an optionaldest
property.filesSrc
: an array of strings representing only thesrc
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')
});