Manage Files - 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 2. Manage Files

We’ve spent a little time with Grunt covering the basics, but running fun little JavaScript programs doesn’t really show off what Grunt can do. When you’re using Grunt in one of your projects, it’s very likely you’ll do some work with the file system. For example, you might read some settings in from a file, or you might write your own files to disk. You might need to create files and folders or copy files around. You can do all of these things by calling out to the operating system yourself using Node.js, but Grunt provides tools to do it easily. In this chapter we’ll explore those tools as we build a simple Grunt task to copy a manifest of files in our project to a working folder that we can upload to our web server.

Creating the Project

Let’s create a project folder called deployment and then navigate into it:

$ mkdir deployment

$ cd deployment

$ npm init

Fill in the basic information or leave it at its default values. Alternatively, create your own package.json file that contains this:

{

"name": "deploying",

"version": "0.0.0",

"description": "A simple project to copy files for deployment."

}

Next, we add Grunt as a dependency:

$ npm install grunt --save-dev

Now we’ll create a basic Gruntfile.js with the following content:

files/simple/deploying/Gruntfile.js

module.exports = function(grunt){

}

Now let’s create a few simple files and folders in this project. First, create an index.html file with a default HTML5 template:

files/simple/deploying/index.html

<!DOCTYPE html>

<html lang="en-US">

<head>

<meta charset="utf-8">

<title>Test page</title>

<link rel="stylesheet" href="stylesheets/style.css">

</head>

<body>

<h1>Test page</h1>

</body>

</html>

Then create the folders stylesheets and javascripts:

$ mkdir stylesheets

$ mkdir javascripts

After that, create a stylesheets/style.css file with the following contents:

files/simple/deploying/stylesheets/style.css

h1{color: #F00;}

We’re not putting much in the files for this exercise; we just want some text in there so we’ll know later on that the files copied correctly. If we left them blank we wouldn’t be sure if the right things got copied.

Finally, create the file javascripts/app.js, which looks like this:

files/simple/deploying/javascripts/app.js

var app = {};

app.name = 'Hello';

With the setup out of the way, let’s jump into how we work with these files in Grunt tasks.

Creating and Deleting Directories

For our task to copy files from their original location to a destination directory, we need to create that destination directory. And every time we want to re-create the destination folder we’ll need to delete it and its contents. So let’s use Grunt’s built-in tools to create two tasks—one to create the folder and one to delete it.

Specifying Configuration Options

Grunt provides grunt.config.init, which lets us define the configuration for our Grunt tasks by passing in a JavaScript object with the properties and values for our tasks.

files/simple/deploying/Gruntfile.js

grunt.config.init({

});

When you install and configure a Grunt plug-in, you’ll often have to add some properties and values to this configuration object. Typically you’ll add a property for the specific plug-in, and then that property will have its own configuration object.

While we’re not building a plug-in here, let’s follow the same approach. We’ll create a property called copyFiles and place our configuration variables within that object.

A best practice for creating configuration options for a task is to place all options within an options property. This avoids any potential collision with Grunt’s API.

Let’s define our first option. We need a way to specify the destination folder that we’ll copy our files to, so we’ll create an option for the workingDirectory:

files/simple/deploying/Gruntfile.js

copyFiles: {

options: {

workingDirectory: 'working',

}

}

We’re going to leave a trailing comma after the value when we’re writing configuration options. That way we won’t forget to add it when we add a new option to this section later. However, this is not valid according to the specifications for ECMAScript 6. Grunt and Node.js won’t complain, but JavaScript syntax checkers (and seasoned JavaScript developers) might. When you’re done writing your configurations, you’ll definitely want to remove trailing commas.

Creating a Folder

Grunt’s built-in grunt.util.mkdir method creates folders, and so all we have to do is create a task, read the name of the directory from our configuration object, and create the folder.

files/simple/deploying/Gruntfile.js

grunt.registerTask('createFolder', 'Create the working folder', function(){

grunt.config.requires('copyFiles.options.workingDirectory');

grunt.file.mkdir(grunt.config.get('copyFiles.options.workingDirectory'));

});

We’re using grunt.config.requires to ensure that the configuration property we want has been set. The task will abort if the field isn’t specified. Notice that we can use a string with dot notation to look up properties in the configuration object. We then use grunt.config.get to fetch the value out of the object and use it to create the folder, using the same dot notation.

At the command line we can run

$ grunt createFolder

and we’ll see the new working folder in our directory.

Removing Folders

To remove the working folder, we can write a very similar task, but this time we’ll use Grunt’s grunt.file.delete method instead. This deletes a file, or a folder and all of its contents.

files/simple/deploying/Gruntfile.js

grunt.registerTask('clean',

'Deletes the working folder and its contents', function(){

grunt.config.requires('copyFiles.options.workingDirectory');

grunt.file.delete(grunt.config.get('copyFiles.options.workingDirectory'));

});

One of the biggest advantages of using these Grunt utilities instead of the raw operating-system commands is that they will work on multiple operating systems. The syntax for recursively deleting folders is very different between Linux and Windows.

Now let’s look at how we copy the files over.

Copying Files

Our project may have lots of files that we don’t want to deploy to the web server. For example, there’s no need to send up our Gruntfile.js or the node_modules folder if we’re building a basic website. So we’ll need to tell Grunt what files we want to copy over. Let’s create a new manifestproperty of our copyFiles configuration object, which will be an array of file paths we want to copy.

files/simple/deploying/Gruntfile.js

copyFiles: {

options: {

workingDirectory: 'working',

*

manifest: [

*

'index.html', 'stylesheets/style.css', 'javascripts/app.js'

*

]

Grunt provides grunt.file.copy, which lets us specify a source file and a destination. Unlike the grunt.file.delete method, it doesn’t handle folders. We’ll address that later. For now we’ll just be very explicit and list every file we want in our manifest property.

Our copyFiles task will check for the workingDirectory and manifest properties and then iterate over the files in the manifest, copying each file into the working folder.

files/simple/deploying/Gruntfile.js

grunt.registerTask('copyFiles', function(){

var files, workingDirectory;

grunt.config.requires('copyFiles.options.manifest');

grunt.config.requires('copyFiles.options.workingDirectory');

files = grunt.config.get('copyFiles.options.manifest');

workingDirectory = grunt.config.get('copyFiles.options.workingDirectory');

files.forEach(function(file) {

var destination = workingDirectory + '/' + file;

grunt.log.writeln('Copying ' + file + ' to ' + destination);

grunt.file.copy(file, destination);

});

});

We can run this task with

$ grunt copyFiles

Running "copyFiles" task

Copying index.html to working/index.html

Copying stylesheets/style.css to working/stylesheets/style.css

Copying javascripts/app.js to working/javascripts/app.js

Done, without errors.

and the files are copied into our working folder.

Now, to make this all fit together nicely, let’s create a new task that runs the clean, createFolder, and copyFiles tasks. Let’s call the task deploy, shall we?

files/simple/deploying/Gruntfile.js

grunt.registerTask('deploy', 'Deploys files',

['clean', 'createFolder', 'copyFiles']);

Run this new task:

$ grunt deploy

Running "clean" task

Running "createFolder" task

Running "copyFiles" task

Copying index.html to working/index.html

Copying stylesheets/style.css to working/stylesheets/style.css

Copying javascripts/app.js to working/javascripts/app.js

Done, without errors.

You’ll see that all of the tasks ran in the order we specified, and all of the files were copied into the working folder as expected. Technically, we don’t need the createFolder task; Grunt’s grunt.file.copy will create the destination folder if it doesn’t exist. But it’s best to be specific.

So far we’ve demonstrated that we can use Grunt’s built-in file tools to copy individual files, but what we’ve built here works only if we specify the individual files we want to copy. That’s not practical in a lot of cases. We might want to copy a folder and all of its files.

Recursive File Copying

Let’s modify the configuration section of Gruntfile.js so it specifies folders as well as files:

files/recursive/deploying/Gruntfile.js

grunt.config.init({

copyFiles: {

options: {

workingDirectory: 'working',

manifest: [

'index.html', 'stylesheets/', 'javascripts/'

]

}

}

});

If we tried to run our tasks right now we’d get errors because Grunt’s built-in copy doesn’t support directories. But we can iterate over the files and folders in our list and then detect if the entry is a file or a folder. If it’s a file we can copy it like before, but if it’s a folder we’ll just have to iterate over the files and folders inside that folder. If you’ve done anything like this in other languages, you’ll know that the solution is to use recursion. And Grunt provides a built-in function for that.

Currently, our copyFiles task loops over the files like this:

files/simple/deploying/Gruntfile.js

files.forEach(function(file) {

var destination = workingDirectory + '/' + file;

grunt.log.writeln('Copying ' + file + ' to ' + destination);

grunt.file.copy(file, destination);

});

But let’s change this task so it instead calls a function we’ll create, called recursiveCopy:

files/recursive/deploying/Gruntfile.js

files.forEach(function(item) {

recursiveCopy(item, workingDirectory);

});

The recursiveCopy function takes in the source, which is either a file or folder, and the destination folder. This function then checks to see if the source is a file or a folder. If it’s a file, we’ll copy it. But if it’s a folder, we’ll have to dig into the files in the folder and call the recursiveCopyfunction again. And if there are folders within folders, we’ll have to handle those the same way. But thanks to the power of recursion, we can declare the function like this:

files/recursive/deploying/Gruntfile.js

var recursiveCopy = function(source, destination){

if(grunt.file.isDir(source)){

grunt.file.recurse(source, function(file){

recursiveCopy(file, destination);

});

}else{

grunt.log.writeln('Copying ' + source + ' to ' + destination);

grunt.file.copy(source, destination + '/' + source);

}

}

We use grunt.file.isDir to detect whether the source element is a file or a directory. If it’s a file, we print the file to the screen and do the file copy like before.

When the file is a directory, we use grunt.file.recurse, which runs the callback function of our choice against each file or folder in the structure. When grunt.file.recurse executes the callback, it sends the source file’s absolute path as the first parameter. It can send the root directory, the current file’s directory, and the current file’s name as arguments if the callback function accepts them. But in our case we’ll keep things really simple; we just pass the source to our recursiveCopy function inside the callback.

A quick run of our copyFiles task shows it’s working:

$ grunt copyFiles

Running "copyFiles" task

Copying index.html to working

Copying stylesheets/layout.css to working

Copying stylesheets/style.css to working

Copying javascripts/app.js to working

Done, without errors.

By combining a little JavaScript code with Grunt’s built-in utilities, we can now clone a directory structure with ease.

Accessing Configuration inside Tasks

Using grunt.config.get to retrieve values from inside of tasks works fine, but Grunt provides a handy shortcut. Inside a task, we can access this.options to access the options property of our task’s configuration. This lets us to change the tasks’ name without having to change how we get the task’s associated parameters.

files/options/deploying/Gruntfile.js

files = this.options().manifest;

workingDirectory = this.options().workingDirectory;

Grunt also provides this.requiresConfig as a shortcut for checking configuration. Unfortunately, unlike this.options, the this.requiresConfig method doesn’t look up values relative to the task. But by using this.name to fetch the task name, we can remove all of the hard-coded configuration in the task, like this:

files/options/deploying/Gruntfile.js

grunt.registerTask('copyFiles', function(){

var files, workingDirectory;

*

this.requiresConfig(this.name + '.options.manifest');

*

this.requiresConfig(this.name + '.options.workingDirectory');

*

*

files = this.options().manifest;

*

workingDirectory = this.options().workingDirectory;

files.forEach(function(item) {

recursiveCopy(item, workingDirectory);

});

This is a nice refactoring of the copyFiles task. But we can’t do this for the clean and createFolder tasks since those use the copyFiles task’s configuration values. We’ll have to leave those alone.

Using Values from Files

Occasionally we’ll want to use some of the values from package.json in our projects, such as the project name, the project author, or the license information. Grunt provides a function called file.readJSON that, as you might be able to guess from the name, reads JSON data from a file and parses it into a JavaScript object. Add this to the configuration section of the Gruntfile:

files/recursive/deploying/Gruntfile.js

grunt.config.init({

*

pkg: grunt.file.readJSON('package.json'),

It’s very common to see something like this in a Gruntfile. We can use these and other configuration values throughout the Gruntfile in a couple of ways. First, we can just access them as properties of the configuration, like grunt.config.get(’pkg.name’). But we can also use Grunt’s templating engine.

When we copy files to the source folder, let’s add a version.txt file that includes the name of the app and the version.

At the bottom of the copyFiles task, add this code:

files/recursive/deploying/Gruntfile.js

var content = '<%=pkg.name %> version <%= pkg.version %>';

content = grunt.template.process(content);

grunt.file.write(workingDirectory + '/version.txt', content);

The grunt.template.process function injects our configuration variables into the template string, giving us a new string that we can write into a file.

These template strings work automatically inside the Grunt configuration section too, without the need for the explicit call to grunt.template.process.. That means you can use templating to easily access configuration variables. For example, if you wanted to use the value of thecopyFiles.options.manifest variable in another task’s configuration, you could reference it as <%= copyFiles.options.manifest %> instead.

Now, if we run grunt copyFiles again, in our working folder we’ll get a new file called version.txt that contains this:

deploying version 0.0.0

This technique is great for injecting the name of the project, the authors, or even the licensing information into the headings of files.

What’s Next?

Grunt’s built-in file utilities make it easy to work with files and folders across operating systems. They form the foundation of many of Grunt’s plug-ins. In addition, the templating mechanism can ease configuration and text generation. Before you move on, try these additional things:

· Inside the copyFiles task, use this.requires to make the copyFiles task depend on the clean task.

· Break the code that creates the version.txt into its own task. Make sure it depends on the successful completion of the copyFiles task and add your new task to the deploy task.

Now let’s look at multitasks, a powerful Grunt feature that many plug-ins rely on heavily.