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
- being outdated or not maintained at all
- possibly unstable
- too opinionated / not opinionated enough
- complicated configuration
- long execution time
Integrating Grunt into MavenIn 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
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:
- 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.
- In the generate-resources phase, the “default” Grunt task is run.
Setting up Grunt TasksAdd 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.