Configuring tasks

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.

Configuring multitasks

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.

Configuring options

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.

Configuring files

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 match

All most people need to know is that foo/*.js will match all files ending with .js in the foo/ subdirectory, but foo/**/*.js will match all files ending with .js in the foo/ 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.

Single set of source files

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.

Multiple sets of source files

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.

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. 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 to true to enable the following options:

cwd All src matches are relative to (but don't include) this path.

src Pattern(s) to match, relative to the cwd.

dest Destination path prefix.

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."

Refer to Code example 07-config-files for demonstrations of each file configuration method.

Templates

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.122.244