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.
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.
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 be darwin, 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.
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.
18.226.150.245