How to use Grunt to automate repetitive tasks


Grunt is a task runner. What that means is that it will open a specified program, perform specified tasks on your files, close the program, move to the next task on the list, then repeat until the end of the list. For each project you might lint (proofread) your css and js files, concatenate all your js files so they’ll load once, minimize your css and js files to make them smaller and load faster, compress your image files so they’ll load faster, and create several sizes of a mobile icon. Grunt can automate this process — you no longer need to do each task manually one by one. In this article, we’ll completely set up grunt on our system and make it run one task for us. I’ll explain how I got each piece of the puzzle so we’ll know how to set up the other tasks.

Helpful sites:
http://gruntjs.com/
http://www.hongkiat.com/blog/automate-workflow-with-grunt/
http://www.codereadability.com/jshint-with-grunt/
http://gruntjs.com/getting-started

1. This learning process is with Cordova in mind. So create a sample Cordova project after setting the default folder in the console. Open the console or terminal window. (In Windows: Start > Command Prompt):

cordova create myApp
cd myApp
cordova platform add android
npm update -g cordova

Full instructions of the above process: https://iphonedevlog.wordpress.com/2014/06/20/using-cordova-3-5-cli-on-mac-os-x-mavericks-to-build-android-apps/

2. For a test, I opened the myApp/www/js/index.js file and removed the final curly braces. We’ll have grunt run JSHint and see if it detects the errors. If it does, then our setup worked.

3. We’ll install the grunt program with the global -g key so it’ll be available no matter what folder we start it from. Type and hit Enter:

npm install -g grunt-cli

(or, sudo npm install -g grunt-cli, and give Admin password.)

4. Add two blank text files to the project if they have not already been generated. Both of these files belong in the root directory of the project (where the www folder is): package.json and Gruntfile.js (Gruntfile.js has a capital G.)

5. Open package.json in a text editor and add the following basic text:

{
  "name": "myApp",
  "version": "0.1.0",
  "description": "Tasks to run for Cordova projects",
  "main": "Gruntfile.js",
  "devDependencies": {
    "grunt": "~0.4.5"
  }
}

6. Open Gruntfile.js in a text editor and add the following basic text:

module.exports = function(grunt) {
    grunt.registerTask('default', [] );
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json')
    });
};

7. Decide what plugins you want to use to automate tasks for your project. What manually repetitive tasks do you want to automate? Visit http://gruntjs.com/plugins and enter keywords in the Search box.

These would be relevant to my projects:
a. npm install grunt-contrib-jshint –save-dev (validate JS files — see if there are any problems I missed in Sublime Text)
b. npm install grunt-contrib-uglify –save-dev (minify JavaScript files with UglifyJS to make them faster loading)
c. npm install grunt-postcss –save-dev (prefix css; it will add all the browser vendor prefixes so that I don’t have to)
d. npm install grunt-contrib-imagemin –save-dev (minimize images to make them faster loading)
e. npm install grunt-contrib-concat –save-dev (concatenate similar js files so they load once per page, saving server calls)
f. npm i -g grunt-cli-cordova –save-dev (creates the icons for a Cordova project)

Adding grunt-contrib-jshint

8. Let’s install one of the above sample grunt plugins and make it work. Type into the console and hit Enter:

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

9. Open package.json. By using “–save-dev”, the npm program has added the plugin information as a devDependency to the file: “grunt-contrib-jshint”: “^1.0.0”

See it here:

{
  "name": "myApp",
  "version": "0.1.0",
  "description": "Tasks to run for Cordova projects",
  "main": "Gruntfile.js",
  "devDependencies": {
     "grunt": "^1.0.1", // comma added here
     "grunt-contrib-jshint": "^1.0.0"
   }
 }

If the red text above was not added, add it manually. Make sure the file really is a text file and not rtf, for instance.

10. Open Gruntfile.js and add the above plugin to the file as:

module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-jshint');

I have included the entire grunt file at the bottom of the article so you know where to put everything. Please read through the entire article, though, because there are steps to take other than filling out the grunt file.

11. Also register the jshint program to tell grunt what tasks we want it to do: grunt.registerTask(‘default’, [‘jshint’] );

See it here:

module.exports = function(grunt) {
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.registerTask('default', ['jshint'] );
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json')
    });
};

12. Let’s add what tasks we want Grunt to do with grunt-contrib-jshint. That information goes in the grunt.initConfig section above. This means we need to read the grunt-contrib-jshint page on the npm plugins site to learn about those options. Head over to http://gruntjs.com/plugins and go to the plugin page, type jshint in the search box, and find the link to our plugin page. (Or put grunt-contrib-jshint in Google search to find it faster.)

13. If you search in npm, you’ll get an npm page. (If by Google, you may find the information displayed in GitHub.) The page tells us what to put in the Gruntfile. There will be several options based on what we want to do. In addition, the jshint program needs to know where the target js files are.

The Usage Example section recommends this syntax:

grunt.initConfig({
  jshint: {
    all: ['Gruntfile.js', 'lib/**/*.js', 'test/**/*.js']
  }
});

My js files are in myApp/www/js, and I will be referencing grunt from /myApp, so I change my Gruntfile to:

module.exports = function(grunt) {

    grunt.loadNpmTasks('grunt-contrib-jshint');
 
    grunt.registerTask('default', ['jshint'] );
 
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'), // add a comma here!
        
          jshint: {
            all: [
                'Gruntfile.js', 
                'www/js/*.js'
            ]
          }
    });
 
};

14. On this page we learn that grunt-contrib-jshint uses a program called JSHint; it assumes that it has already been installed and ready to use. The grunt plugin does not contain the JSHint program; it merely acts as a task runner — it starts it up, runs it, then shuts it down and runs the next program listed in Gruntfile.js. In the same way, you do not dry the clothes yourself, but load the clothing into the dryer and turn it on. Just like you and the dryer are different entities, so grunt and jshint are different entities. It merely “flips the switch,” so to speak; it doesn’t actually supply the washer capability. This is why grunt doesn’t work when you merely add the sample text to the gruntfile as in the tutorial examples.

Go ahead and install JSHint in the console as noted in http://jshint.com:

npm install jshint -g --save-dev

(Or sudo npm install jshint -g –save-dev and supply Admin password.)

15. grunt-contrib-jshint tells us to use the following format in Gruntfile.js:

{
  "undef": true,
  "unused": true,
  "predef": [ "MY_GLOBAL" ]
}

What are “undef,” “unused,” and “predef”? Remember that grunt will open JSHint and use that program to work on your file, and grunt will use the options particular to JSHint. We find JSHint’s options on this page: http://jshint.com/docs/options/ (This link was given in the grunt page in npm.) To activate an option, we add the option name and set it to “true,” as shown above. The above three terms are found on that page. Carefully scroll down the page to see what options JSHints can use on your js file.

16. Through my extremely limited understanding of JavaScript and JSHint, I chose the following options. The explanations in parentheses are from the JSHint options page:
a. maxdepth (how nested or indented I want my blocks to be.)
b. nonbsp (This option warns about “non-breaking whitespace” characters. These characters can be entered with option-space on Mac computers and have a potential of breaking non-UTF8 web pages.)
c. strict (This option requires the code to run in ECMAScript 5’s strict mode. Strict mode is a way to opt in to a restricted variant of JavaScript. Strict mode eliminates some JavaScript pitfalls that didn’t cause errors by changing them to produce errors. It also fixes mistakes that made it difficult for the JavaScript engines to perform certain optimizations.)
d. undef (This option prohibits the use of explicitly undeclared variables. This option is very useful for spotting leaking and mistyped variables.)
e. unused (This option warns when you define and never use your variables. It is very useful for general code cleanup, especially when used in addition to undef.)

My Gruntfile.js result:

'www/js/*.js'
], // add comma
options: {
    "maxdepth": 2,
    "nonbsp": true,
    "strict": "implied",
    "undef": true,
    "unused": true
}

Note the placement of commas after each option except for the last.

Finally, my entire Gruntfile looks like this:

module.exports = function(grunt) {

// Tell grunt we want to use this plugin
    grunt.loadNpmTasks('grunt-contrib-jshint');

// Tasks we want grunt to do when we type "grunt" into the console    
    grunt.registerTask('default', ['jshint'] );
    
    grunt.initConfig({
// Configurations go here
    pkg: grunt.file.readJSON('package.json'),
        
          jshint: {
            // the files to lint
            all: [
                'Gruntfile.js', 
                'www/js/*.js'
            ],
            // configure JSHint
            options: {
                "maxdepth": 2,
                "nonbsp": true,
                "strict": "implied",
                "undef": true,
                "unused": true
            }
          }
    });
 
};

17. To start the task runner, type the following into the console:

grunt

I got a list of each .js file and the problem with each one, identified by line number. JSHint did not fix the problems, but pointed them out. So I went to http://jshint.com/ and copied/pasted the erring file into the screen. On the right side of the screen, the errors were pointed out; I kept fixing until there were no more errors.

Validating grunt

If you get Gruntfile errors, you can validate the Gruntfile syntax here: http://esprima.org/demo/validate.html It was somewhat helpful when I got errors in the console like “>>SyntaxError: Unexpected identifier” or “>>SyntaxError: Unexpected token ;”

You can validate your json file here: http://jsonlint.com/

I myself received all the following errors while learning grunt. I pasted each one into a search engine to figure them out:

ERROR SyntaxError: Unexpected identifier
Warning: Task “default” not found
‘module’ is not defined
Warning: Task “jshint:files” failed
Warning: Task “jshint:all” failed

I never got all of them fixed (such as “‘module’ is not defined” and “Warning: Task “jshint:all” failed”); yet the script ran and worked, so I consider it successful. (Actually, these were unexpectedly resolved when I added another task to grunt. See below.)

Adding more tasks to grunt

When adding more plugins on a page, we would add them in the following format. I highly recommend adding and testing one at a time to make debugging easier.

In package.json, add the new program to a new line:

"grunt-contrib-jshint": "^1.0.0",  // add a comma here
"grunt-contrib-cssmin": "~0.9.0"

In Gruntfile.js:

// Tell grunt we want to use this plugin
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-uglify'); // add a new line for new task

// Tasks we want grunt to do when we type "grunt" into the console    
    grunt.registerTask('default', ['jshint','uglify'] ); // add all the tasks on this line, each separated by a comma
    
    jshint: {
    ...
    }
    
    uglify: { // add new tasks named and enclosed like this
    ...
    }

Of course, you still have to download the dependencies that grunt refers to.

Re-using your grunt task list

Notice a new folder added to your root: myApp/node_modules. Open it to see the modules you’ve installed.

What if I wanted to re-use the Gruntfile for other projects? I would keep this myApp as my grunt command center, copying/pasting a new project’s www folder in place and running grunt from there. Once fixed, copy the changed files back to the project’s own folder.

Running only part of your grunt task list

If we just want to run one of the several tasks in the Gruntfile, we can add these last lines, for example, if we want to run only jshint or uglify:

grunt.registerTask('default', ['jshint','uglify','imagemin','concat'] );
grunt.registerTask('hint', ['jshint'] ); // replace default with hint; list only one task
grunt.registerTask('ugly', ['uglify'] ); // replace default with ugly; list only one task

In these two lines, ‘hint’ and ‘ugly’ are aliases that you make up. By typing grunt hint, only jshint will run. Typing grunt ugly will run only the uglify task.

Why would you run a grunt script of only one or two tasks when you’ve gone to the trouble of assembling 5 or 6 tasks in the list? Well, you might run the whole script once, fix any css and JS errors it found, then run only those css and JS tasks to have them double-check your fixes. If the complete list includes compressing images and creating icons, it won’t make sense to do those steps again.

Adding grunt-postcss

I am adding to the Gruntfile process above. I sought to install this plugin, grunt-postcss (because I wanted autoprefixer). Check out its page at https://github.com/nDmitry/grunt-postcss

1. This page tells us to install the plugin with this text in the console/terminal:

npm install grunt-postcss --save-dev

2. Then it tells us to run the following in the terminal to add three more packages: pixrem, autoprefixer, and cssnano:

npm install grunt-postcss pixrem autoprefixer cssnano

If we look into the node-modules folder, we’ll see folders for those three programs. Combined, they contain over a thousand files. If we look in package.json, we’ll see a line added for grunt-postcss. However, we don’t see lines for the other three programs. That’s probably because they are programs (processors), not tasks.

We are told what to put in the Gruntfile to activate grunt-postcss (I put this right after the closing curly brace of the jshint section, and added a comma to it):

postcss: {
 options: {
 map: true
 },

processors: [
 require('pixrem')(), // add fallbacks for rem units
 require('autoprefixer')({browsers: 'last 2 versions'}), // add vendor prefixes
 require('cssnano')() // minify the result
 ]
 },
 dist: {
 src: 'css/*.css'
 }

We are told how to populate Gruntfile with options for grunt-postcss. For instance, “options.failOnError” means you’ll look for options: { in Gruntfile and add failOnError: false to include it. If there is an option already there (such as map: true), then you’ll add a comma to the previous one to make it a list (e.g., map: true,).

This grunt-postcss replaces the standard autoprefixer and the page tells you what to do if you already have the autoprefixer processor installed.

I added ‘postcss’ to the registerTask line to get:

grunt.registerTask('default', ['jshint','postcss'] );

Then added the loadNpmTask line like this:

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

Now for the warnings growing pains.

3. I made several changes going forward. To test JSHint, I opened Cordova’s www/js/index.js and removed the last };

4. I added test.css and inserted some css that I thought would need prefixes.

5. I changed the following in the postcss dist section to match my folder setup:

 dist: {
 src: 'css/*.css'
 } );

… was changed to …

'www/css/*.css'

I changed the following

 options: {
 map: true
 },
 processors: [

… to …

options: {
 throwError: true,
 processors: [

6. I ran grunt. Only JSHint found errors. No mention of postcss running, though. A warning was issued: Warning: Task “jshint:all” failed. Use –force to continue. 

If you get Warning: pattern.indexOf is not a function then go below and compare your file with mine, or copy/paste my file version over yours.

7. So I ran grunt –force. This time autoprefixer ran and listed a number of issues with the two css files. Autofixer not only pointed out the errors, but corrected the file so far as it was able — the console directed me to make other changes manually, such as, “use some JS grid polyfill for full spec support.” The file was minimized — all the text on a single line.

Sample changes in the sample css file:

.theFlex {
    display: flex;
}

… became …

.theFlex{display:-ms-flexbox;display:flex}
.blink {
    animation:fade 3000ms infinite;
    -webkit-animation:fade 3000ms infinite;
}

… became …

.blink{animation:a 3s infinite;-webkit-animation:a 3s infinite}
.theGrid {
    display: grid;
}

… became …

.theGrid{display:-ms-grid;display:grid}

Remember that the warning generated was: Warning: Task “jshint:all” failed. Use –force to continue. I commented out the options for jshint and found that if I commented out these lines:

//    "strict": "implied",
//    "undef": true,

… then it ran without errors.

Final working Gruntfile for jshint and postcss:

module.exports = function(grunt) {

// Tell grunt we want to use this plugin
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-postcss');

// Tasks we want grunt to do when we type "grunt" into the console    
    grunt.registerTask('default', ['jshint','postcss'] );
    
    grunt.initConfig({
// Configurations go here
    pkg: grunt.file.readJSON('package.json'),
        
        jshint: {
            // the files to lint
            all: [
                'Gruntfile.js', 
                'www/js/*.js'
            ],
            // configure JSHint
            options: {
                "maxdepth": 2,
                "nonbsp": true,
                "unused": true
            },
          },
          
        postcss: {
            options: {
                throwError: true,
                processors: [
                    require('pixrem')(), // add fallbacks for rem units
                    require('autoprefixer')({
                        browsers: ['last 2 versions'] // add vendor prefixes
                        }),
                    require('cssnano')(), // minify the result
                ],
            },
            dist: {
                src: 'www/css/*.css' // was _css/*css
            }
        }
    });
 
};

The above processes both tasks without terminal errors.

Add another Grunt task! https://iphonedevlog.wordpress.com/2016/11/11/2128/

Advertisements

3 thoughts on “How to use Grunt to automate repetitive tasks

  1. Very nice writeup. However, perhaps due to carelessness on my part, I had the following problems:
    1. Fatal error: Unable to find local grunt.
    npm install fixed this, maybe. I have no idea why.
    2. Warning: Task “default” not found. Use –force to continue. So I used force.
    3. Syntax error, perhaps in gruntfile.js: Unexpected token ILLEGAL (rather unhelpful, eh?)

    And finally, you had to use JSHint directly to find/fix the problems. So why use grunt in the first place?

    Hope this helps

    • I’m sorry you had problems. I had written out the instructions while I underwent the process, then updated as I figured out each error. Then I started over again from the top to see if I can reproduce the instructions. Then I posted.

      However, I am having problems again with a different plugin, and looking it up online just sends me in circles. The error messages are pretty vague, unfortunately, but grunt is not alone in that.

      As to why use grunt at all if you need to open JSHint anyway? I suppose if you had JSHint in addition to 5 other programs, it would make more sense, but not if you used JSHint by itself. Thank you for checking out the article.

  2. Pingback: iPhone Dev Log

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s