Starting from scratch with Grunt

an introduction to getting your project setup with the Grunt task runner

Posted by Wazoo on December 01, 2018 · 26 mins read

Within the current maelstrom of web development, you’ve probably heard of Grunt, the JavaScript task runner that runs on Node.js. Maybe you’ve tried to get it up and running, but got lost or confused during setup. Perhaps you’ve known about it, but you’re not sure if it’s the right fit for your project in 2018.

I’m going to show you how Grunt can still be a successful addition to your toolbelt today. I’ll show you how to get started, and go from zero to Grunt-o!

Before you start

  • Ability to make a basic website with HTML, CSS, and JavaScript.
  • Using the terminal / console window in Mac / Linux / PC.

Goals

  • Understand enough about Node.js to get Grunt up and running
  • Create a Sass file
  • Utilize Grunt to compile multiple Sass files into one CSS file, apply any prefixes, and minify.
  • Minify your JavaScript.
  • Run one command that will “host” your webpage and watch your entire directory for updates.

What is a “Task Runner”?

Even with the availability of Webpack, Grunt and Gulp are pretty much the two most popular JavaScript task runners. The purpose of a task runner is to automate pieces of your workflow, to make creating and debugging web applications a whole lot more efficient.

What is the difference between Gulp and Grunt?

You might have noticed that I also have a similar Task Runner article using Gulp. You might be asking yourself, What is the difference between the two? Is there one?.

When I think of Gulp, I think of the concepts of pipes or streams. In other words, most of the way Gulp works is to use Node streams within its internals. Try to imagine setting a small toy boat on a gently moving river stream. Imagine how the boat travels down stream adjusting to any small current or debris along the way. A Node stream is somewhat similar; instead of a toy boat, we have a JavaScript or CSS file. As this file goes down the stream, we’re applying different modifications to it.

Grunt operates in a somewhat similar fashion, however its internal mechanism relies on temporary files instead of streams.

In terms of longevity and maintenance, both task runners aren’t really going anywhere however Gulp gets a slight edge in terms of being currently active in 2018+. While Grunt and its plugins are all available, they are on life support. This is by no means a detractor from using or learning Grunt, it’s still a useful and handy addition.

Installation

Before we can go into working with Grunt, we’ve got to get some tooling installed to help us out.

Install Node.js

You can install either the LTS or the current release of Node.js. If you’re on a Mac, you can install Node via Homebrew:

brew install node

Otherwise, grab the download you want to install on your platform of choice. Using all of the defaults is fine during the installation.

Install Grunt CLI globally

We can finally install Grunt. Sorta. We’re not actually installing Grunt per-se, but the Grunt command line interface (CLI). This basically just allows your computer to recognize the grunt command at all times. However, without a local install in your current project, you won’t be able to use Grunt.

npm install -g grunt-cli

While this has been quite a bit of setup to work through, the best part of this is that you only need to do this once. Node and the Grunt-CLI just need to be available on your system.

From this point on, you don’t need to install or worry about them again.

Create Your Project

Let’s go ahead and create a new project folder to work in. In your terminal window, create a folder:

mkdir grunt-and-sass
cd grunt-and-sass

As you may already know, Node manages the libraries for your application, known as packages, via the help of NPM - literally the Node Package Manager.

Note: Just for fun, visit https://npmjs.com and notice the banner in the top-left corner of the site. Hit refresh a few times to see a different definition of the NPM abbreviation.

It’s not crazy important to go into too many details about NPM right now, but I think it’s helpful to know that NPM will help you use and manage the huge amounts of cool JavaScript packages available for you.

npm init --y

This creates a default package.json file in the current directory. The package.json file is the main configuration file for pretty much everything JavaScript. This command you’re using just creates a starter package.json for you, without bothering you for any details…

{
  "name": "grunt-and-sass",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

We’ll go through the whole package.json file in the future, but for now notice that the name field has been populated automagically with the name of the folder you’re in. The scripts object is going to be important later on, but we’ll leave that for now.

Let’s use NPM to install Grunt!

npm install grunt --save-dev

After some lines scroll by in the terminal console, you should now see a change in the package.json with the addition of a devDependencies object:

  "devDependencies": {
    "grunt": "^1.0.4"
  }

Congratulations! Grunt is now available in your project! It should be available to us to use right away!

Back in the terminal, just type:

grunt

What happened? The Grunt task runner ran, but we should be seeing a message in our console, A valid Gruntfile could not be found.

No problem, let’s fix that right away. In the terminal window, type:

touch Gruntfile.js

This will create an empty file for us in the current directory, called (you guessed it!) Gruntfile.js.

a basic Gruntfile

Open the file in your editor and type in the following:

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

The Grunt task runner uses the Gruntfile.js as it’s main configuration file to rely on the tasks you’re trying to create and run via Grunt.

This main configuration file will always receive the grunt object as its only parameter upon initialization.

Within this function that we are using in Grunt, we need to always call initConfig which is run using an object we are using to define all of the tasks we want Grunt to handle.

This should hopefully start to make sense as we add more tasks to it.

In this particular snippet, we’re defining a key of pkg and using a Grunt helper method to load our package.json file.

Running grunt again will produce a new error message: Warning: Task “default” not found.

defining the default Task

Grunt makes use of a helper method called registerTask to create a Task. A Task is a unique name given to a set of stuff that you want Grunt to perform for you. This task name needs to be unique, as it’s the way you reference which Grunt task you’d like to run from the command line.

Grunt doesn’t make any assumptions about what kind of tasks you want it to handle in your project. Grunt only requires you specify ONE task as your “default”.

Let’s do this now. Update your Gruntfile.js:

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

Now when you type grunt on the command line, you should see… Done (yay!)

loading some default Grunt plugins

Part of the real power behind Grunt is its widely available library of plugins available. A Grunt plugin is merely a library or function that has a very specific job to perform. To use an example, one plugin that we’ll be using is the grunt-contrib-uglify plugin. The single function of this task plugin is to minify (ie. compress) the JavaScript source files in our web application.

While some Grunt tutorials and projects use the loadNpmTasks method, you soon run into a messier Gruntfile.js as you add more plugins.

I really like the load-grunt-tasks plugin which manages the loading of Grunt plugins on your behalf, while keeping for a neater Gruntfile.js.

npm install load-grunt-tasks --save-dev

Then make a reference to it in the Gruntfile.js:

// Load Grunt
module.exports = function(grunt) {

    // Load all grunt tasks matching the ['grunt-*', '@*/grunt-*'] patterns
    require('load-grunt-tasks')(grunt);
    
    grunt.initConfig({
      pkg: grunt.file.readJSON('package.json'),
      // Tasks
    });
    
    // Register Grunt tasks
    grunt.registerTask('default', []);
};

Another useful plugin I recommend, is time-grunt, which displays a friendly readout of how long individual Tasks are taking Grunt to perform.

npm install time-grunt --save-dev

Just make sure it’s the first plugin being referenced in the Gruntfile.js:

// Load Grunt
module.exports = function(grunt) {

    // require it at the top and pass in the grunt instance
    require('time-grunt')(grunt);

If you now run grunt, you’ll see a friendly time readout.

basic web application

To help continue demonstrating how Grunt is useful for your web application, let’s now include a basic HTML template, along with some CSS and some JavaScript. Not a terribly huge web application by any stretch, but it’s going to help provide some clarity.

Create 3 files: index.html, styles.css and entry.js.

touch index.html
touch styles.css
touch entry.js
// style.css
body {
}

canvas {
    background-color: #ff0000;
}
// entry.js
window.onload = function() {
    var root = document.getElementById('root');
    var el = document.createElement('canvas');
    el.width = '400';
    el.height = '400';
    root.appendChild(el);
};
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link href="style.css" rel="stylesheet" />
    </head>
    <body>
        <div id="root"></div>
    </body>
    <script src="entry.js"></script>
</html>

So you might have already guessed, but the only thing this template does, is display a red square.

bringing in grunt-contrib-connect

First, let’s add a Grunt plugin to use as a small web server on our machine. As you might imagine this is a common need for a web developer. A popular plugin to handle this in Grunt is grunt-contrib-connect.

npm install grunt-contrib-connect --save-dev

To specify a Grunt plugin configuration, it’s a standard practice to simply use the name of the plugin as the configuration object. To show you what that means, open up the Gruntfile.js and add the configuration for the plugin we just added - connect.

      // snip!
      pkg: grunt.file.readJSON('package.json'),
      
      connect: {
          server: {
            options: {
                port: 3000,
                hostname: '*',
                base: '.',
                keepalive: true
            }

          }
          
      }

Once you’ve created the object with the name of the plugin you’re using (connect), the next “child” object to define is a target name. This becomes useful when you have different configurations for different environments. In this particular case, we’re using the name server as our target name. Now within the server object, is an options object containing some key value pairs defining the properties of our small local web server.

If we now run grunt connect:server on the command line, you should see some output about the local web server. You should be able to open http://localhost:3000 in your favorite browser. Notice the server name matches the server sub-object within connect.

You can also use grunt connect, as server is the only child object - so by default it will be used.

To help demonstrate this, just copy and paste the server object and call it anything else – tardis - for example.

    connect: {
      server: {
        options: {
            port: 3000,
            hostname: '*',
            base: '.',
            keepalive: true
        }
    
      },
      tardis: {
        options: {
            port: 4000,
            hostname: '*',
            base: '.',
            keepalive: true
        }
    
      }
    }

So try out:

  • grunt connect:server - to ensure the original configuration still works
  • grunt connect:tardis - to see how to specify a different configuration (tardis is on port 4000)
  • grunt connect - what will happen?

grunt-contrib-uglify

Now let’s minify the JavaScript source in our tiny web application. Using grunt-contrib-uglify should accomplish this.

npm install grunt-contrib-uglify --save-dev

As with grunt-contrib-connect, we need to specify a configuration object within Gruntfile.js.

uglify: {
    src: {
      options: {
        sourceMap: true,
        sourceMapName: './dist/bundle.min.map'
      },
      files: {
        './dist/bundle.min.js': ['entry.js']
      }
    }
  }

We’re defining an uglify object with several options defined. A sourceMap is not always required, but it can help when trying to trace down an error in the browser’s JavaScript console. The files object is letting us specify which files to use. We are taking the entry.js file and outputting the result to create bundle.min.js in the dist folder which will be created if it doesn’t exist.

You might start to realize at this point that since we are generating an output for the JavaScript, we need to update the index.html to make sure we reference it.

  <script src="dist/bundle.min.js"></script>

So now to double check run:

You should still see our favorite red square.

adding sass support via grunt-contrib-sass

I promised in our intro that we would be delving into SASS support, let’s do it and pull in the grunt-contrib-sass plugin.

mv style.css style.scss
npm install grunt-contrib-sass node-sass --save-dev

As you noticed, we’ve renamed our current CSS stylesheet to style.scss which is the common extension for SASS support.

// style.scss

$font-color: #434343;
$red-color: #ff0000;

canvas {
    background-color: $red-color;
}

h1 {
    color: $font-color;
}

Just some basic usage of SASS variables to make sure we can’t reference this stylesheet directly.

Now let’s create the sass object definition in the Gruntfile.js:

sass: {
    dist: {
      files: {
        'dist/style.css': 'style.scss'
      }
    }
  },

As with the JavaScript source file reference we had to update in the index.html, we’ll have to do the same thing for our dist/style.css.

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

So now you should be able to run our Grunt tasks:

Minifying the CSS with grunt-contrib-cssmin

Let’s now add the ability to minify our CSS using the grunt-contrib-cssmin plugin. grunt-postcss also comes in handy here to give us the ability to apply autoprefixer to our CSS styles to account for vendor prefixes, etc.

npm install grunt-postcss grunt-contrib-cssmin autoprefixer --save-dev

postcss configuration

I’m going to pull in autoprefixer to add vendor prefixes and minify the CSS.

postcss: { 
  options: {
    map: false,
  },
  dist: {
    src: 'dist/style.css'
  }
},

defining the cssmin object configuration

cssmin: {
  target: {
    files: [{
      expand: true,
      cwd: 'dist',
      src: ['*.css', '!*.min.css'],
      dest: 'dist',
      ext: '.min.css'
}]
  }
},

NOW we can finally run all of our Grunt tasks together:

  • grunt sass
  • grunt postcss
  • grunt cssmin
  • grunt uglify
  • grunt connect
  • opening the page at http://localhost:3000

Hurrah! However we definitely do not want to type that in every time we make changes to our web application; otherwise it defeats the entire purpose of using Grunt!

Let’s change this now.

back to our default grunt task

So in the start of our Grunt section, we specified an empty Grunt default task:

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

You’ll notice the empty Array as the second parameter. This now comes in handy as we can list the Grunt tasks that we want to use when the default Task is used. Let’s create a few Tasks to help us out:

grunt.registerTask('build', [
    'sass',
    'postcss',
    'cssmin',
    'uglify'
]);

grunt.registerTask('serve', [
    'build',
    'connect'
]);

grunt.registerTask('default', [
    'build'
]);

For a bit of flexibility, we’re defining 3 Tasks - build, serve and default:

  • grunt build - this Task is solely for sassifying and minifying our CSS and JS file(s).
  • grunt serve - this Task calls the build Task, then calls the connect task to start our local web server.
  • grunt - the default Task is just to run our build Task

The whole Gruntfile.js is here for your reference and pleasure:

// Load Grunt
module.exports = function(grunt) {

    // require it at the top and pass in the grunt instance
    require('time-grunt')(grunt);

    // Load all grunt tasks matching the ['grunt-*', '@*/grunt-*'] patterns
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
      pkg: grunt.file.readJSON('package.json'),
      
      connect: {
          server: {
            options: {
                port: 3000,
                hostname: '*',
                base: '.',
                keepalive: true
            }

          }
      },

      cssmin: { 
        target: {
          files: [{
            expand: true,
            cwd: 'dist',
            src: ['*.css', '!*.min.css'],
            dest: 'dist',
            ext: '.min.css'
            }]
        }
      },

      postcss: {
        options: {
          map: false
        },
        dist: {
          src: 'dist/style.css'
        }
      },

      sass: {
        dist: {
          files: {
            'dist/style.css': 'style.scss'
          }
        }
      },

      uglify: {
        dist: {
          options: {
            compress: true,
            mangle: true,
            sourceMap: true,
            sourceMapName: './dist/bundle.min.map'
          },
          files: {
            './dist/bundle.min.js': ['entry.js']
          }
        }
      }
    });

    grunt.registerTask('build', [
        'sass',
        'postcss',
        'cssmin',
        'uglify'
    ]);

    grunt.registerTask('serve', [
        'build',
        'connect'
    ]);
    
    grunt.registerTask('default', [
        'build'
    ]);
};