Create Project Scaffolds - 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 6. Create Project Scaffolds

When you decide to work on a new project, you’ve got this great idea in your head and you can’t wait to turn it into code. But sometimes the monotony of setting up folders, configuration files, and other required bits can slow your momentum considerably. Modern web projects have a ton of tooling that needs to be set up. For example, if you’re going to use Grunt you’ll need a package.json file for your project, and you’ll want a Gruntfile.js with your tasks already configured. And if you’re doing any kind of unit testing you’ll want your setup files for your test suite. You may even want to include JavaScript libraries. We can define our own project templates that give us just what we need.

The grunt-init command gives us the power to create new projects with ease. In Chapter 1, The Very Basics, you learned about the npm init command that walks you through creating a new package.json file for your project. The grunt-init command uses a similar wizard-based system to help you create your own projects from templates. These templates can be distributed as plug-ins via npm, but these templates can also just be folders on your hard drive. In this chapter we’ll look at an existing template and then we’ll build our own template from scratch, which you can then modify to meet your own needs.

Before we begin, we need to install the grunt-init with npm globally so it’s available everywhere on our systems. We do that through npm like this:

$ npm install -g grunt-init

Test it out by typing this:

$ grunt-init

It should report that no templates are found. That’s expected, as we don’t have any system-wide templates created.

Using Existing Templates

You install templates into a folder in your home folder. This will be ~/.grunt_init/ on OS X and Linux, and %USERPROFILE%\.grunt-init\ on Windows. Typically, since these templates are often located on GitHub, you’ll use Git to clone the template into that folder. If you don’t have the Git client installed, you can get installers for your operating system at the Git website.[19]

Let’s clone the grunt-init-grunt template, which makes creating a new Gruntfile easy. First, we clone the template file to our machine’s ~/.grunt-init/ folder. Open a new Terminal and ensure that you’re in your home folder. Then use Git to clone the grunt-init-gruntfile plug-in into the folder.grunt-init/gruntfile within your home folder:

$ git clone https://github.com/gruntjs/grunt-init-gruntfile.git \

.grunt-init/gruntfile

And now we can try it out. When we run the template we’ll be asked a series of questions, very similar to the ones we get when we use the npm init command. Our answers to the questions determine the values that end up in the Gruntfile and package.json file for the project.

$ grunt-init gruntfile

Running "init:gruntfile" (init) task

This task will create one or more files in the current directory, based on the

environment and the answers to a few questions. Note that answering "?" to any

question will show question-specific help and answering "none" to most questions

will leave their values blank.

"gruntfile" template notes:

This template tries to guess file and directory paths, but you will most likely

need to edit the generated Gruntfile.js file before running grunt. If you run

grunt after generating the Gruntfile, and it exits with errors, edit the file!

Please answer the following:

[?] Is the DOM involved in ANY way? (Y/n) n

[?] Will files be concatenated or minified? (Y/n) n

[?] Will you have a package.json file? (Y/n) y

[?] Do you need to make any changes to the above before continuing? (y/N) n

Writing Gruntfile.js...OK

Writing package.json...OK

Initialized from template "gruntfile".

Done, without errors.

We can write our own questions and use those answers to include, exclude, and alter the content of new files. So let’s dig in to creating our own template!

Creating a Custom Template

Let’s create a template that sets up a basic HTML5 website with a single JavaScript file and a single stylesheet. We’ll also add in a couple of additional prompts that let users decide if they’d like a Gruntfile, and if they’d like some default content added to the stylesheet. Our project will utilize the values the users provide in the JavaScript and the HTML content, too.

A template consists of a file called template.js that contains the main script executed by grunt-init, and a folder called root that contains the files that will make up the project. This root folder can have HTML files, JavaScript files, images, and pretty much anything else you think you might find useful. The following figure shows how the process will work:

images/files/template.png


Figure 3. How our template works

Navigate to the .grunt-init folder in your home folder. Create a new folder called html5template within the .grunt-init. Then, inside this new folder, create a folder called root, which will contain all of the source files for our template:

$ cd .grunt-init

$ mkdir html5template

$ cd html5template

$ mkdir root

Next, create the template.js file. This is where the logic for our script will go. We’ll use this file to define the questions we’ll ask the user, along with the default answers, and we’ll determine exactly how to process the template. We’ll start out by defining a description of the template on the screen, along with some notes.

scaffolding/html5template/template.js

exports.description = 'Creates an HTML5 template with CSS and ' +

'JavaScript files.';

exports.notes = 'This project includes a default JavaScript and CSS file' +

'In addition, you can choose to include an optional ' +

'Gruntfile and some default CSS styles';

The description is displayed when you type the command grunt-init without any arguments. This command lists all the installed templates and their descriptions. The notes get displayed when you actually run the template.

The template itself is a basic function that takes in a grunt object, plus an init object and the done object that is used for asynchronous processing. We’ll talk about how that works in Chapter 3, One Task, Many Outputs.

exports.template = function(grunt, init, done) {

};

Inside of that function, we execute the init.process method, which takes an options object, an array of input prompts, and a callback function that does the actual processing. Our template is very basic, so we’ll pass in a blank options object. We’ll also define an empty array of prompts, which we’ll fill in shortly.

init.process({}, [

// input prompts go here

], function(err, props) {

// processing section

});

Now, let’s look at how we can ask the user for input.

Prompting for Input

The input object has a prompt function that defines a request for information from the user. There are many built-in ones, including the project’s name, the project’s author, the main starting point of the app, and even an open source library.

Inside of the empty array we created when we defined the init.process function, add these lines:

// input prompts go here

// Prompt for these values.

init.prompt('name' , 'AwesomeCo'),

init.prompt('author', 'Max Power'),

We’re prompting for the name, the author, and the main file for the project. We can specify the default values for each of these as well.

Using Variables in Templates

The variables for the name and author are available in every file in the root folder. All we have to do is embed the values like this:

{%= name %}

So, let’s create the template for the HTML page. In the root/index.html file, add this content:

scaffolding/html5template/root/index.html

<!DOCTYPE html>

<html lang="en-US">

<head>

<meta charset="utf-8">

*

<title>{%= name %}</title>

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

</head>

<body>

<header>

*

<h1>{%= name %}</h1>

</header>

<section>

<p>Your content goes here.</p>

</section>

<footer>

*

<small>Copyright {%= grunt.template.today('yyyy') %} {%= author %}</small>

</footer>

<script src="javascripts/app.js"></script>

</body>

</html>

The highlighted sections show how we’re using the name and author data we’ll prompt users for when they run this template.

Look at this line:

scaffolding/html5template/root/index.html

<small>Copyright {%= grunt.template.today('yyyy') %} {%= author %}</small>

Grunt provides some special methods designed for use in templates. We can easily inject the current year into our template with grunt.template.date. Passing in "yyyy" gives us the four-digit year.

Let’s do something similar with our JavaScript file. Create a root/javascripts/app.js file with this content:

scaffolding/html5template/root/javascripts/app.js

/*

* {%= name %}

*/

var app = {};

app.name = '{%= name %}';

We can use the input data in JavaScript code, too. Here we embed the name of the project as a name property of an app object.

No web project would be complete without a stylesheet, so let’s add one. Create a stylesheets folder inside of the root folder:

$ mkdir root/stylesheets

Then create a stylesheet called app.css in that folder. We’ll put in a simple rule that removes the default margin and padding from the body element:

scaffolding/html5template/root/stylesheets/app.css

body{

margin: 0;

padding: 0;

}

Any file or folder we put inside our template’s root folder will get copied into the destination location, and so we could add more default stylesheets or scripts, like the Bootstrap framework, jQuery Mobile, or even something custom built.

Processing the Template

With the template files in place, we can turn our attention to the callback function in template.js. Add these lines to the body of the callback function of init.process:

var files = init.filesToCopy(props);

init.copyAndProcess(files, props);

init.writePackageJSON('package.json', props);

done();

First, we get the list of files we’re going to process. This puts into an array the paths of all the files in the root folder and its child folders. Then we use the init.copyAndProcess function to copy all the files and process their contents. The properties we set get passed along to this function and get used in the views.

Finally, the package.json file gets written, using the properties we prompted for.

Let’s run this and see how it works. In a new Terminal, create a new folder called test, navigate into that folder, and run the grunt-init command with our template’s name:

$ mkdir test

$ cd test

$ grunt-init html5template

Running "init:html5template/" (init) task

This task will create one or more files in the current directory,

based on the environment and the answers to a few questions. Note

that answering "?" to any question will show question-specific

help and answering "none" to most questions will leave their values

blank.

"html5template" template notes:

This project includes a default JavaScript and CSS file. In

addition, you can choose to include an optional Gruntfile and some

default CSS styles.

Please answer the following:

[?] Project name (AwesomeCo)

[?] author (Max Power)

[?] Main module/entry point (index.html)

[?] Do you need to make any changes to the above before

continuing? (y/N)

Writing index.html...OK

Writing javascripts/app.js...OK

Writing stylesheets/app.css...OK

Writing package.json...OK

Initialized from template "html5template".

Done, without errors.

And now when you look at the contents of the current folder, you’ll see the generated files. The HTML file will contain values where the variables were. Let’s take this a step further and see how we can skip files based on user input.

Including Files Conditionally

So far we’ve used the built-in prompts, but it might be nice if we let the users decide if they want us to generate a Gruntfile for their projects. We’ll assume they do by default, but we’ll give them the option to exclude it. We can do that with a custom prompt and a custom property.

First, ensure you’re back in the html5template folder that contains your template files. Then add the file Gruntfile.js to the root folder that contains the following code:

scaffolding/html5template/root/Gruntfile.js

module.exports = function(grunt){

grunt.initConfig({

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

});

}

This sample Gruntfile loads the package.json file into the variable pkg. It’s a handy way to avoid repeating ourselves; we can now easily use the data in the package.json file inside of a Gruntfile. (We used this back in Using Values from Files.) Since this is such a common practice, we want this in our template’s Gruntfile.

Next, in template.js, in the prompts array, after the prompt for the name, author, and main file, add the following code to ask users if they’d like a Gruntfile:

scaffolding/html5template/template.js

{

name: 'gruntfile',

message: 'Do you want a Gruntfile?',

default: 'Y/n',

warning: 'If you want to be able to do cool stuff you should have one.'

},

This is an example of a custom prompt. We use a JavaScript object that contains the name, the prompt’s message, the default value, and a warning message that is displayed to users if they don’t choose a valid option.

We’ve asked for the value, so now let’s use it. In the processing callback function, we’ll need to turn the “yes” or “no” value the user entered into a Boolean. So, above the init.copyAndProcess line, add this line:

props.gruntfile = /y/i.test(props.gruntfile);

Then right beneath that, add the logic to evaluate that variable, which is just a JavaScript if statement:

var files = init.filesToCopy(props);

*

if(props.gruntfile){

*

props.devDependencies = {

*

'grunt': '~0.4.4'

*

};

*

}else{

*

delete files['Gruntfile.js'];

*

}

If the user wants a Gruntfile, we make sure we add Grunt as a dependency to the package.json file. If he doesn’t want a Gruntfile, then we remove the Gruntfile from the list of files we’re going to copy. We always put every file possible into the template’s root folder, and then we filter out what we don’t want to copy.

Now when we run the command, we’ll get the new prompt. If we answer yes, we’ll get the Gruntfile, and Grunt gets added to our package.json file as a development dependency.

We can include or exclude files, but we can also include or exclude parts of our template files using a similar approach.

Including File Contents Conditionally

Using a custom prompt and some logic in the template.js file, we’ve been able to conditionally include or exclude a file from our template’s root folder. But we can also use conditional logic in our template files.

The CSS property box-sizing: border-box is becoming quite popular. By default, an element’s width equals the actual width of the element plus the margin, padding, and borders. That can make it really tricky to do math. But with border-box, an element’s width is the defined width, and the padding, margins, and border do not affect the width. This makes doing columns a lot easier. However, it’s not supported everywhere. So let’s add a prompt to our configuration to let users decide if they want to use this rule. Add this new prompt to the template.js file, right below the prompt for the Gruntfile:

scaffolding/html5template/template.js

{

name: 'gruntfile',

message: 'Do you want a Gruntfile?',

default: 'Y/n',

warning: 'If you want to be able to do cool stuff you should have one.'

},

*

{

*

name: 'borderbox',

*

message: 'Do you want to use the border-box styling in CSS?',

*

default: 'Y/n'

*

},

Then, in the processing section, below the property evaluation for the Gruntfile, add this line to handle the border-box property:

props.gruntfile = /y/i.test(props.gruntfile);

*

props.borderbox = /y/i.test(props.borderbox);

Finally, add the following code to root/stylesheets/app.css:

scaffolding/html5template/root/stylesheets/app.css

body{

margin: 0;

padding: 0;

}

{% if (borderbox) {%}

/* apply a natural box layout model to all elements

* http://www.paulirish.com/2012/box-sizing-border-box-ftw/

*/

*, *:before, *:after {

-webkit-box-sizing: border-box;

-moz-box-sizing: border-box;

box-sizing: border-box;

}

{% } %}

Our definition for border-box is wrapped in an if statement; it’ll be written only if the user sets the property! It’s that easy to do conditional content. Just watch out for the syntax of the if statement here—it’s very easy to forget one of the curly braces.

Running grunt-init html5template again now results in this additional question, and if we answer yes, our CSS file will have the border-box code. If we answer no, our CSS file will be blank.

The rename.json File

The file rename.json lets you map files in your template’s root directory to destination locations. You can even change the names of the files using template strings. Here’s an example from the grunt-init-jquery plug-in:

{

"src/name.js": "src/jquery.{%= name %}.js",

"test/name_test.js": "test/{%= name %}_test.js",

"test/name.html": "test/{%= name %}.html"

}

You could specify a completely different destination folder, which might let you better organize the files in your template.

What’s Next?

Grunt templates are incredibly powerful if you do a lot of new-project work. They can be a great way to bootstrap a project of any type, too. You could use them to generate a project in any language, for any reason you see fit. And you can share your templates with the world. Before moving on, though, explore these additional topics:

· Right now, files that exist are automatically overwritten when we run the template again. The exports.warnOn property lets us specify a pattern of files that should not be overwritten. If the template runner encounters any files in the current folder that match this pattern, it will abort the script. Add this into the script at the top to prevent overwriting any file.

· Modify the template so that the object in the JavaScript file is included only if the user requests it. Make a new prompt and a new property, and then optionally include the source code.

· Include a README.md file in Markdown format in the project that specifies the project name and description. Create a new prompt that asks for the project description to populate the value.

· Modify the template so that it optionally includes support for Sass and CoffeeScript based on user prompts. Use the Grunt configuration you built in Chapter 4, Build a Workflow, as your guide. As an extra step, if the plug-in uses CoffeeScript, include a CoffeeScript file instead of the JavaScript file. If the user requests Sass, include a Sass file instead of the CSS file. Remember to alter the watch task so that it invokes the tasks to compile CoffeeScript or Sass files.

At this point you should feel much more comfortable with how Grunt works. Look at the projects you maintain and investigate how Grunt can improve your development and deployment workflow. Use the multitude of well-tested plug-ins available, but don’t be afraid to create your own templates and plug-ins whenever it makes sense.

From here, you might want to look at other projects, such as Yeoman, which is a project generator that relies on Grunt. Yeoman makes it a snap to create modern web projects.[20] Another great option is Lineman, which provides a thin wrapper around Grunt, creating an awesome workflow for client-side web applications.[21] You’ll find those projects easy to use with your newfound understanding of Grunt.

$ grunt automate:everything

Footnotes

[19]

http://git-scm.com/downloads

[20]

http://yeoman.io/

[21]

http://linemanjs.com/