Aug 27, 2014

Integrating Grunt into an existing Maven build

We love automating things. Maven has been our build tool for quite some time now and it has served us very, very well. Ever-repeating tasks like setting up a new project from scratch, building it, testing it, running it, integrating it into our existing CI, etc. are a chore. Maven helps us focusing on the actually interesting bits while removing the necessity to manually deal with the mundane stuff.

But especially when developing web applications, not everything is shiny and golden with Maven. Back in the good ol’ days, web resources were simple files that lived somewhere in your src/main/webapp directory. Well, those days have long but passed: JS and CSS files are (finally) treated as “proper” sources that need some care during the project’s build phase, too:
  • Linting  JS code with tools like e.g. JSHint
  • Possibly generating JS code from another language, e.g. TypeScript
  • Testing JS code
  • Aggregating JS modules into a single, browser-consumable package
  • Pre-processing LESS or SASS into actual CSS
  • Auto-generating CSS sprites
  • Automatically adding the proper vendor prefixes to the CSS Output
  • Minifying / compressing JS and CSS
  • Optimizing Images
  • ...
Maven was not built with these kinds of tasks in mind and – naturally – often falls short of our expectations. There are some helpful Maven plugins, of course, but most of them sicken from the same basic problems:
  • being outdated or not maintained at all
  • possibly unstable
  • too opinionated / not opinionated enough
  • complicated configuration
  • long execution time
The web ecosystem around Node.js has spawned lots of tools that deal with all kinds of tasks. Grunt (and hundreds of plugins available via npm) is one of the most prominent ones. And since it runs on Node.js – which is known for its quick start-up and execution time – in many cases the performance is superior to the “classic” Maven plugins that use Rhino, custom Ant tasks or other means.

Integrating Grunt into Maven

In order to run Grunt tasks, you’d normally need Node.js and npm. Even though I strongly recommend setting those tools up on your dev machine, you don’t strictly have to: The great frontend-maven-plugin by Eirik Sletteberg allows us to integrate Grunt tasks into our Maven build even without a local installation of Node.js and npm. It also tries to download the correct Node.js binary for the current system - enabling the Maven build to run on your CI as well (as long as that machine has access to the internet).

In this example, we basically want to do two main things:
  • Handle Scripts
    • Running JSHint against the entire JS code
    • Minifying everything into a single file
  • Handle Styles
    • Generating the CSS from LESS
    • Running autoprefixer on the result to achieve dynamic cross-browser functionality
    • Minifying the result into a single file
Our goal is to generate the final JS / CSS files from the sources and then copy those file to the appropriate folder inside Maven’s target folder. This way, Maven will eventually pick up those files and put them into the final artifact (WAR, JAR, etc.).

First, you’ll need a package.json right next to your pom.xml. This file tells npm what packages you need for the build. Especially the devDependencies are the interesting part. Aside from the basics, we also require several grunt-contrib-* or grunt-* plugins. Those plugins can be loaded by Grunt at build time to do specific things (e.g. the LESS compiler).

Now, let’s configure the aforementioned maven plugin. Add it to the list of plugins in your pom.xml’s <build> section. That adds two additional executions to your lifecycle:
  1. The plugin will attempt to figure out the current environment and download the Node.js binary along with npm (if they haven’t already been downloaded before, of course). This happens in Maven’s initialize phase.
  2. In the generate-resources phase, the “default” Grunt task is run.
This basically completes the Maven part. Now we only need to set up the Grunt tasks.

Setting up Grunt Tasks

Add a file called gruntfile.js next to your pom.xml and package.json. This file basically serves three purposes: loading all required plugins, building the configuration object and defining Tasks.

Most of the time, plugins will simply be loaded via grunt.loadNpmTasks which uses the devDependencies in the package.json file. You can utilize the full Node.js module loading mechanism here as well because the gruntfile is just a Node.js module itself.

Every plugin expects to find its configuration under a meaningful key in the Grunt configuration object (e.g. grunt-contrib-jshint will look for the key “jshint”). You will need to look up the respective documentation for each plugin you want to use. Keep in mind that most plugins can (or must) have multiple named “tracks” in their configuration. We will use this later to run only a specific part of the grunt-contrib-copy’s plugin configuration – one for scripts and one for styles.

Finally, defining tasks is simply just giving a meaningful name to a sequence of tasks. You could call each task individually from the console, of course:

grunt jshint # run all tracks of the grunt-contrib-jshint plugin
grunt uglify # same for grunt-contrib-uglify
grunt copy:scripts # copy the minified JS file to the target Folder

But it’s obviously easier to simply define a “scripts” task that does all that for us. We recommend to have one top-level task for each pipeline – in our example that’s “scripts” and “styles” – as well as one “build” task that runs every of those top-level pipeline. Finally, the “default” task should basically just call the “build” task followed by a possible “clean” task that removes intermediate artifacts – in our case the dist Folder.

Here is one example of a gruntfile that’s doing all we need. As you can see, the sheer file size and complexity quickly gets out of hand. By utilizing the power of the Node.js module system and the API which is available to us, we can move each plugin’s configuration into its own file and clean up some repetitive clutter as well. This is a slightly refactored version of the same gruntfile. For example, the JSHint task’s configuration file would look like this. You don’t need to re-invent the wheel, of course: there are helper plugins and tutorials for effectively modularizing and speeding up Grunt tasks, too.