Grunt configuration can be thought of as single JavaScript object, though, instead of assigning values, we'll use functions provided by Grunt to get and set properties.
We briefly touched on Grunt configuration in Chapter 1, Introducing Grunt, displaying simple uses of the grunt.initConfig
, grunt.config.get
and grunt.config.set
functions. The grunt.initConfig
function (which as mentioned earlier, is aliased from grunt.config.init
) accepts an object, which is then used as the starting point for our configuration, whereas the grunt.config.get
and grunt.config.set
functions are used to get and set individual properties. We can also use the shorter grunt.config
function, which works like jQuery getters and setters. When called with one argument it aliases to grunt.config.get
, and with two arguments, it aliases to grunt.config.set
. Each line in the following example is functionally equivalent:
grunt.config.init({ foo: { bar: 7 }}); grunt.config.set('foo.bar', 7); grunt.config('foo.bar', 7);
It's important to note that calls to grunt.config.init
(or grunt.initConfig
) will erase all prior configuration.
Grunt has a focus on declaratively defining a build. Since we use a build tool to improve our effectiveness, for our team and ourselves, it's important our build is manageable and accessible. If our build tool were simply a long shell (or batch) script of many steps, each process would be defined imperatively in sequence. Its length would make it difficult for others (and our future selves) to understand, forcing us to reverse engineer the steps. Whereas if our build were made up of declarative steps, we could read it like, "I'd like to compile these CoffeeScript files into this folder using these options". For example:
coffee: { compile: { files: { 'build/app.js': 'src/scripts/**/*.coffee' }, options: { bare: true } } }
Therefore, we can view a Grunt configuration as a way to declaratively define how we wish to run imperative Grunt tasks. This is where Grunt shines and why it has become so popular – it helps to abstract the "how" and focuses on the "what".
The Grunt configuration methods may be used anywhere with access to the grunt
object, however, in most cases we will only use our configuration within tasks and multitasks.
As described previously, Grunt tasks are just functions. For example, let's say we have a Grunt task to check for stray console.log
statements in our app.js
file. This consoleCheck
task may look like:
//Code example 06-config-get-set // tasks/console-check.js module.exports = function(grunt) { grunt.registerTask('consoleCheck', function() { //load app.js var contents = grunt.file.read('./src/app.js'), //search for console.log statements if(contents.indexOf('console.log(') >= 0) grunt.fail.warn('"console.log(" found in "app.js"'), }); };
However, we may wish to reuse this task in another project. To assist with reusability, we'll generalize this task to be a string checking task by allowing us to define which file and what string to look for:
//Code example 06-config-get-set // tasks/string-check.js module.exports = function(grunt) { grunt.registerTask('stringCheck', function() { //fail if configuration is not provided grunt.config.requires('stringCheck.file'), grunt.config.requires('stringCheck.string'), //retrieve filename and load it var file = grunt.config('stringCheck.file'), var contents = grunt.file.read(file); //retrieve string to search for var string = grunt.config('stringCheck.string'), if(contents.indexOf(string >= 0)) grunt.fail.warn('"' + string + '" found in "' + file + '"'), }); };
First, in our new stringCheck
task, we're using grunt.config.requires
to ensure our configuration exists, next we're retrieving this configuration, and finally we'll search for the string and display the result. We can now configure this task to perform its original purpose by providing the following configuration:
//Code example 06-config-get-set // Gruntfile.js grunt.initConfig({ stringCheck: { file: './src/app.js', string: 'console.log(' } });
When running this example with console.log(
in our app.js
file, we should see the following output:
$ grunt Running "stringCheck" task Warning: "console.log(" found in "./src/app.js" Use --force to continue. Aborted due to warnings.
On the last line of our output, we see that Grunt was aborted due to warnings. Since we used the grunt.fail.warn
function in our task, we see the hint to use the --force
flag to continue; however, if we were to use the grunt.fail.fatal
function, we would not be able to ignore our new task until we remove the offending string. See the code examples to view the runnable version.
Also note this is a naïve approach to checking source code. For instance, this task would incorrectly fail when our string was commented out. To resolve this issue, we would need to use a JavaScript parser to extract the code's Abstract Syntax Tree (AST), and then search this tree for syntax of our choosing.
Continuing with our string checker task, we will most likely want to check more than one file. Instead of the file string, we may initially consider using a files array; however, what if we wanted to check for numerous strings in a single file? Should we also convert the
string
property into an array? And what if we only wanted to look for certain strings in certain files? Using arrays would not suffice.
Enter multitasks. Multitasks allow us to solve the hypothetical problems outlined previously using configuration targets. Targets provide us with the means to configure multiple runs of a single task. If we were to convert our string checker task into a multitask, its configuration might look like:
grunt.initConfig({ stringCheck: { target1: { file: './src/app.js', string: 'console.log(' }, target2: { file: './src/util.js', string: 'eval(' } } });
We may recall similar configurations from Chapter 1, Introducing Grunt, code examples, which also use target1
and target2
. These generic names are used on purpose to reinforce the notion that target names may be arbitrarily set. We should therefore devise target names that improve the readability of our build. Many examples on the Internet display task names, such as dist
(short for distribution), build
, and compile
. Although these might describe the target well, for a Grunt newcomer, it can be hard to discern which portions of the configuration must be static and which can be dynamic. For example, in the previous snippet of code, we could have used app
and util
instead of target1
and target2
. These logical names would improve the usability of our build by allowing us to use readable commands like:
$ grunt stringCheck:app $ grunt stringCheck:util
In Chapter 3, Using Grunt, we'll learn more on running and creating our own multitasks.
Defining options to customize a task is quite common; hence, both tasks and multitasks have their function context set to the Task
object, which has an options
function available. When called, it looks for the task's configuration by name and then looks for the options
object. For example:
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. For example:
grunt.initConfig({ myMultiTask: { options: { foo: 42, bar: 7 }, target1: { }, target2: { options: { bar: 8 } } } });
As target1
does not have an options
object defined, retrieving its options will yield: { foo:42, bar:7 }
. However, when we retrieve target2
options, its bar option will override the task options and the resulting object will be: { foo:42, bar:8 }
. We will cover more on the Task
object in Chapter 3, Using Grunt.
A vast majority of Grunt tasks will perform some kind of file operation. To cater for this, Grunt uses a predefined object structure along with file "globbing" (or wildcard file selection) to produce a succinct API for describing files. While running a multitask, its configuration will be checked for this file pattern and it will attempt to match the files it describes with what it can find at those locations. Once complete, it will place all matching files in the task's files array (this.files
within the context of a task).
Next, we will cover the various ways that we can describe files for various use cases. Firstly, however, we'll discuss what it means to match a file. File matching within Grunt uses a module written by Isaac Schlueter: node-glob
. File globbing comes from Unix in the 70s, when a simple language was invented to allow wildcard selection of files. For example, *.txt
will match both a.txt
and b.txt
. Here is an extract from the Grunt documentation describing globbing options available in Grunt (http://gswg.io#grunt-globbing):
"While this isn't a comprehensive tutorial on globbing patterns, know that in a filepath:
*
matches any number of characters, but not/
?
matches a single character, but not/
**
matches any number of characters, including/
, as long as it's the only thing in a path part
{}
allows for a comma-separated list of "or" expressions
!
at the beginning of a pattern will negate the matchAll most people need to know is that
foo/*.js
will match all files ending with.js
in thefoo/
subdirectory, butfoo/**/*.js
will match all files ending with.js
in thefoo/
subdirectory and all of its subdirectories."
Next, we'll review portions of Code example 07-config-files
. This example contains one task showTargetFiles
, which displays the files
array of each of its targets:
// Register a multitask (runs once per target) grunt.registerMultiTask('showTargetFiles', function() { // Show the 'files' array this.files.forEach(function(file) { console.log ("source: " + file.src + " -> " + "destination: " + file.dest); }); });
Based on this task, we'll notice that each file
in the files
array contains src
and dest
properties. The src
property in this case is the output from matching the input globs against the filesystem. Each of the following target examples contains src
inputs and also the result of this task.
This is the compact format; the target may describe one set source of files using the src
property, along with an optional destination file using the dest
property. Without a destination, this format is typically used for read-only tasks, like code analysis such as our string checker task:
target1: { src: ['src/a.js', 'src/b.js'] }
We could shorten the preceding example using the {}
syntax described earlier, to denote a
or b
. We will also add a destination property:
target1: { src: 'src/{a,b}.js', dest: 'dest/ab.js' }
Notice that we have left out the array in this second example as it is not required since we're specifying only one glob string. If we wanted to define multiple sets of source files, we would need to use the files
property.
To describe multiple source sets with single destination, we can use the "Files array format". For example, this format could be useful when describing multiple file concatenations:
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", where the files
property is now an object instead of an array, with each key being the destination and each value being the source, as follows:
target1: { files: { 'dest/abc.js': 'src/{a,b,c}.js', 'dest/xyz.js': 'src/{x,y,z}.js' } }
Moreover, when we specify a set of files using an object with src
and dest
, we have the choice to use some additional options; one of these options will allow us to map directories as opposed to files.
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. This is useful when compiling CoffeeScript (or any other source-to-source compilation) and we'd like to maintain the directory structure, whilst still running each individual file via the transform of our choosing. This is done using the expand option. For instance, if we wanted to compress all of our .js
source files into a result set of the .min.js
files, we could manually map each file from one directory to another:
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'}, ], }
However, with each file addition, we will need to add another line of configuration. With the expand
option, a destination for each of matched source files will be automatically generated based on the source file's path and additional options. Here, target2
is equivalent to target1
, however as we add new source files, they will automatically be matched by our '**/*.js'
glob string and mapped to the appropriate destination file:
target2: {
files: [
{
expand: true,
cwd: 'lib/',
src: '**/*.js',
dest: 'build/',
ext: '.min.js'
},
],
}
Here are the additional options available at http://gswg.io#configuring-tasks for use inside any file object:
"expand
Set totrue
to enable the following options:
cwd
Allsrc
matches are relative to (but don't include) this path.
src
Pattern(s) to match, relative to thecwd
.
ext
Replace any existing extension with this value in generateddest
paths.
flatten
Remove all path parts from generateddest
paths.
rename
This function is called for each matchedsrc
file, (after extension renaming and flattening). Thedest
and matchedsrc
path are passed in, and this function must return a newdest
value. If the samedest
is returned more than once, eachsrc
which used it will be added to an array of sources for it."
Refer to Code example 07-config-files
for demonstrations of each file configuration method.
In the preceding configuration section, we covered the use of grunt.config
. Here, we cover one of the reasons why we use a special "getter" and "setter" API to modify a simple object. When we set values in our configuration with grunt.config.set
or grunt.initConfig
, we can use the Grunt template system to reuse other portions of our configuration. For example, if we defined some properties:
//Code Example 08-templates grunt.initConfig({ foo: 'c', bar: 'b<%= foo %>d', bazz: 'a<%= bar %>e' });
Then, if we run the task:
//Code Example 08-templates grunt.registerTask('default', function() { grunt.log.writeln( grunt.config.get('bazz') ); });
We should see:
$ grunt Running "default" task abcde
When we use grunt.config.get("…")
, internally Grunt is using the grunt.template.process
function to resolve each template recursively (that is, we can have templates inside other templates). Grunt templates are most useful when we wish to perform many tasks on a single set of files. We can define this set once and then use Grunt templates to re-use it multiple times. For example, with the following configuration:
//Code example 09-templates-array grunt.initConfig({ foo: ['a.js','b.js','c.js','d.js'], bazz: '<%= foo %>' });
Our preceding task returns:
Running "default" task [ 'a.js', 'b.js', 'c.js', 'd.js' ]
When using the grunt.config.get
function to retrieve the bazz
property, it does not return a string, since bazz
only contains the template reference to foo
. Instead, it is replaced by the foo
array.
18.118.122.244