Create a Plug-in - Pragmatic Bookshelf Publishing Automate with Grunt, The Build Tool for JavaScript (2014)

Pragmatic Bookshelf Publishing Automate with Grunt, The Build Tool for JavaScript (2014)

Chapter 5. Create a Plug-in

As you’ve already seen, plug-ins let you share functionality across projects or with other people, easily extending Grunt’s features. In this chapter we’ll create a simple Grunt plug-in that lets us quickly open in a web browser the page we’re working on. This may seem superfluous, but it lets us explore the process of creating a plug-in, and it allows us to call out to an external program. Plus, when you’re done you’ll have a plug-in you can invoke as part of a larger process. For example, at the end of a build you can make your application’s start page open in the browser so you can see if things are working well.

The Structure of a Plug-in

A basic Grunt plug-in consists of a tasks folder that contains one or more JavaScript files that define the tasks. These files have the exact same structure as a regular Gruntfile.

Inside of the tasks folder is a lib folder that should hold any functions or objects that do the work. In other words, your task definitions go in the main tasks folder and the logic for those tasks goes in the lib folder. If you explore the official Grunt plug-ins you’ll see that this is the structure they use. It keeps the task definitions clean and concise.

The plug-in also has a package.json folder and a Gruntfile.js file. The Gruntfile.js file has a line that loads the tasks from the tasks folder, which makes it easy to try out your plug-in.

Now that you know how plug-ins are structured, let’s build our plug-in’s core functionality.

Creating the Plug-in Skeleton

First, create a new folder called grunt-open-with-chrome. Navigate into that folder and create another folder called tasks. Then run the npm init command to create the package.json file for the plug-in in that new folder:

$ mkdir grunt-open-with-chrome

$ cd grunt-open-with-chrome

$ mkdir tasks

$ npm init

When you run the generator, you’ll have to answer the usual questions. For this exercise, answer them as follows:

1. Leave the project name as the default.

2. For the description, use “Open the page in Chrome.”

3. Leave the version number as the default value.

4. For the project Git repository, leave it as a default or enter a valid Git URL if you’d like to push your code to a Git repository.

5. Leave the project home page, the issue tracker, and the license at their default values.

6. Enter your name for the author name.

7. Enter your email for the author email.

8. Leave all of the other values at their defaults.

Next, run the following to install Grunt, because we’ll want to be able to test out our Grunt plug-ins:

$ npm install grunt --save-dev

Building Our Plug-in’s Logic

Our plug-in’s main goal is to let a user open a web page or a URL with Grunt. Ideally, we’d like it to work like this:

$ grunt open:index.html

And then it would open that page in Google Chrome. To do this, we’ll have to detect the platform we’re running on so we can find out how to launch Chrome, and we’ll have to use some mechanism to make Grunt launch an external program. Grunt comes with grunt.util.spawn, which is perfect for this.

Calling External Apps from Grunt

To call an external file from Grunt, we use Node.js’s built-in child_process module.

The exec method is a perfect fit for this situation. It takes in two arguments: the command we want to run and a callback function that executes when the command finishes. For example, if we wanted to run the ls -alh command, we’d do this:

var exec = require('child_process').exec;

process = exec('ls -alh', function (error, stdout, stderr) {

// whatever we want to do after the program finishes.

});

In the callback, we can check for error messages from the external program and handle them accordingly.

To launch Google Chrome in a way that works on multiple operating systems, we’ll have to dynamically create the command we pass to exec, using different system commands and arguments for each operating system. So let’s build an object that does that for us.

Creating a Module for Our Launcher

To keep the code for our tasks clean, we’re going to encapsulate all of the logic we’ll need to launch Google Chrome in its own Node.js module.

We’ll start by creating a file called lib/chrome_launcher.js that contains the following code:

grunt-open-with-chrome/tasks/lib/chrome_launcher.js

module.exports.init = function(grunt){

// the object we'll return

var exports = {};

// returns the object

return(exports);

};

This is a common pattern in object-oriented JavaScript and in Node.js apps, called the Revealing Module Pattern. We use module.exports to define what objects or functions this module exposes. With the Revealing Module Pattern we define a function that returns a JavaScript object that we create. This allows us to have both public and private methods on this object.

Our main JavaScript program will use this module by requiring it and calling the init function, which then returns the object represented by our exports object. This init function takes a reference to Grunt, and is a very common approach used by authors of Grunt plug-ins.

Inside of the init function we add the function that creates the command based on the operating system the user is running. We use Node’s process.platform method to detect the operating system. If it’s Windows it’ll start with win, and if it’s Linux it’ll start with linux. If it’s a Mac it’ll bedarwin, but we’ll make that the default case. Here’s how we do all of that:

grunt-open-with-chrome/tasks/lib/chrome_launcher.js

// creates the command

var createCommand = function(file){

// booleans for the OS we're using

var command = "";

var linux = !!process.platform.match(/^linux/);

var windows = !!process.platform.match(/^win/);

if(windows){

command = 'start chrome ' + file;

}else if (linux){

command = 'google-chrome "' + file + '"';

}else{

command = 'open -a "Google Chrome" ' + file;

}

return(command);

};

On Windows we use the start command to launch a program. On OS X we have to use the open command, and on Linux we call the program directly. Each of these programs accepts slightly different options, and we have to properly escape the paths to the program and the arguments for each operating system.

Finally, we need to define the public method, which we’ll call open. This method will use Node.js’s exec method to launch Google Chrome. It’ll take the file we want to open as its first argument, and a second argument that references the done function. In Introducing Multitasks, we saw that Grunt doesn’t wait for long-running tasks to finish. We have to tell Grunt to wait until we call the done function. If we don’t do this, we won’t be able to see any error messages because Grunt will quit before the callback on exec can finish.

So, with all that in mind, we define this open method inside the init function as well:

grunt-open-with-chrome/tasks/lib/chrome_launcher.js

// opens Chrome and loads the file or URL passed in

exports.open = function(file, done){

var command, process, exec;

command = createCommand(file);

grunt.log.writeln('Running command: ' + command);

exec = require('child_process').exec;

process = exec(command, function (error, stdout, stderr) {

if (error) {

if(error.code !== 0){

grunt.warn(stderr);

grunt.log.writeln(error.stack);

}

}

done();

});

};

We use the createCommand function to get the command we need for our OS, and then we execute the process with that command. Then in the callback we check to see to see if the process worked. If it returned an exit code of 0, everything went well. If it didn’t, then the program didn’t launch properly. But in either case, that’s where we invoke the done function to tell Grunt we’re finished.

Notice that this method is attached to the module we’re exporting. That will make it visible to the Grunt task. All of the other methods we defined are private ones.

That takes care of the basic implementation. All that’s left is to make it available to Grunt.

The Grunt Task

Our Grunt task needs to take in the filename as its argument and then invoke the open method we just created. Create the file tasks/open_with_chrome.js and add the following code:

grunt-open-with-chrome/tasks/open_with_chrome.js

'use strict';

module.exports = function(grunt) {

};

That should look strikingly similar to what you saw back in Chapter 1, The Very Basics, when you created your first Gruntfile. Remember, a Grunt plug-in is just a Gruntfile stored in a special location.

Now we can require our custom module and define our task. We invoke our open method, passing it the filename from the task along with the done function reference:

grunt-open-with-chrome/tasks/open_with_chrome.js

var chromeLauncher = require('./lib/chrome_launcher.js').init(grunt);

grunt.registerTask('open', 'Opens the file or URL with Chrome',

function(file){

var done = this.async();

chromeLauncher.open(file, done);

}

);

At this point we can test this out. To do that, we’ll create a Gruntfile in the root of our project that contains the typical Grunt boilerplate and a line that loads all of the tasks in the tasks folder. So, create Gruntfile.js as the package.json file:

grunt-open-with-chrome/Gruntfile.js

'use strict';

module.exports = function(grunt) {

grunt.loadTasks('tasks');

};

We can now run the plug-in by typing this:

$ grunt open:Gruntfile.js

Our Gruntfile pops open in the browser. We can specify any file we want, and we can even handle URLs, like this:

$ grunt open:http\://google.com

However, because Grunt uses the colon character as an argument separator, we have to escape the colon with a backslash character or it won’t work.

When we run that command, Google Chrome pops up, displaying the URL we specified! Not a bad bit of work.

You can use this structure on your own Grunt projects too. Instead of putting all of the Grunt tasks in a single Gruntfile, we can modularize them under the tasks folder. Our main Gruntfile can hold all of the configuration and the tasks themselves can be nicely tucked away, out of sight and out of mind.

Specifying Compatibility with Grunt Versions

Grunt is constantly evolving, and you may decide you want your plug-in to support a minimum version of Grunt. The peerDepencencies key lets you do just that:

"peerDependencies": {

"grunt": "~0.4.2"

}

This specifies that this plug-in will work with Grunt 0.4.2 and up, but not Grunt 0.5.0.

Using JSHint to Check for Errors and Problems

We’ve written quite a bit of JavaScript code in this chapter; some of it might not work right, and some of it might not conform to coding standards that other Grunt plug-ins want. We can use JSHint to detect errors and problems so we can fix them.[16] And best of all, we can do it easily with Grunt.

First, we install the grunt-contrib-jshint plug-in:

$ npm install grunt-contrib-jshint --save-dev

Next, we create a file called .jshintrc, which contains the rules we want to test our code against. We’ll use the rules that other Grunt plug-ins use. Place the following lines of code in the file—you don’t have to type in the comments for this to work, but you may want them anyway so you don’t forget what these options do:

grunt-open-with-chrome/.jshintrc

{

"node": true, // For NodeJS

"undef": true, // Require all non-global variables to be declared

"curly": true, // Require curly braces for blocks and scope

"eqeqeq": true, // Require "===" instead of "==" for equality

"immed": true, // Must wrap immediate invoked functions in parens

"latedef": true, // Must define functions and variables before use

"newcap": true, // Constructor functions must be capitalized

"noarg": true, // Don't allow 'arguments.caller' and 'arguments.callee'

"sub": true, // Allow '[]' even if dot notation could be used

"boss": true, // Allow assignments where comparisons might be expected

"eqnull": true // Allow use of '== null'

}

Then we set up the Grunt task for JSHint by including the NodeJS module and configuring its options:

grunt-open-with-chrome/Gruntfile.js

grunt.loadNpmTasks('grunt-contrib-jshint');

grunt.config("jshint", {

all: [

'Gruntfile.js',

'tasks/**/*.js'

],

options: {

jshintrc: '.jshintrc',

},

});

We’re configuring this task to look at all of the JavaScript files within the task folder, no matter how many folders deep they are. This ensures we check the lib folder too. We also let it look at the Gruntfile itself.

And now we can run the following to check for errors.

$ grunt jshint

Running "jshint:all" (jshint) task

>> 3 files lint free.

Done, without errors.

Any errors or warnings found will stop Grunt from processing any other tasks, so this is a great tool to add into a final build process to ensure that you’ve fixed all of your code issues before deploying things to production.

What’s Next?

We’ve built a fairly simple plug-in in this chapter. From here we can publish our plug-in to the npm repository so others can use it. Of course, you’ll want to investigate how to write unit tests for the launching functionality before releasing a Grunt plug-in. Here are a few more things you may want to play with:

· Implement a version of this plug-in that opens the Firefox web browser either instead of Chrome or alongside Chrome.

· Look at the source code for one of the other plug-ins we used in this book and see if you can find ways to improve it; submit any changes to the maintainer.

· We didn’t write any unit tests for our plug-in. Unit-testing JavaScript programs is beyond the scope of this book, but you might want to investigate the Jasmine and Nodeunit testing libraries.[17][18]

Next, let’s look at using Grunt templates to create projects instead of doing all this configuration by hand.

Footnotes

[16]

https://github.com/jshint/jshint

[17]

http://jasmine.github.io/

[18]

https://github.com/caolan/nodeunit