Building Ratpack sites with Grunt

Ratpack is a simple, capable, toolkit for creating high performance web applications using Groovy or Java. Grunt is a Javascript build stytem designed for the web.

Mordern day web applications require a more sophisticated build system. Pre-preocessors like Sass and CoffeeScript, JS dependancy management like Bower and RequireJS and optimisations such as minification and versioning static files are all common build tasks.

Using Grunt to Build

I'm not going to go into great detail about creating a grunt build, there are plenty of better examples on the web already.

lets use the Sample Gruntfile provided by the Grunt website.

Integrating with Ratpack

To run Grunt tasks from Gradle we need to create a custom task, thankfully this is quite simple.

import org.apache.tools.ant.taskdefs.condition.Os  
import org.gradle.api.tasks.Exec

class GruntTask extends Exec {  
    private String gruntExecutable = Os.isFamily(Os.FAMILY_WINDOWS) ? "grunt.cmd" : "grunt"
    def gruntArgs = ""
    public GruntTask() {
        super()
        this.setExecutable(gruntExecutable)
    }
    public void setGruntArgs(String gruntArgs) {
        this.args = "$gruntArgs".trim().split(" ") as List
    }
}

Grunt is built on NodeJS and uses NPM to load all of it's plugins. We need a similar task to run before Grunt to initialise them.

task npm(type: Exec) {  
    group = "Build"
    description = "Installs all Node.js dependencies defined in package.json"
    commandLine = ["npm", "install"]
    inputs.file "package.json"
    outputs.dir "node_modules"
}

Now you can add the the steps into your Gradle Build.

task gruntBuild(type: GruntTask) {  
    group = "Build"
    description = "run dev grunt build"
    gruntArgs = "build"
}
gruntBuild.dependsOn npm  
run.dependsOn gruntBuild  

you should now be able to run a simple Grunt build in your Ratpack application.

Advanced build steps & templates

Now that we have a basic Grunt build running - lets try something a bit more interesting.

One of the key parts to my build is the versioning of static files. This process involves the addition of a checksum to each static asset renaming say logo.png to logo-x34fwedfdsf.png. This allows each asset to have a unique name, and be long cached upstream. A change to the file will result in a new name and thus a new cache entry.

Using a few Grunt plugins we can build a sophisticated solution. I'll be using Rev, Usemin and Copy plugins.

npm install grunt-usemin --save-dev  
npm install grunt-rev --save-dev  
npm install grunt-contrib-copy --save-dev  

As this process will rename the assets referenced in your application templates we must first create a copy of these where we can safely replace those filepaths.

copy: {  
    dist: {
        files: [{
            expand: true,
            dot: true,
            cwd: 'src/ratpack/templates',
            dest: 'target/dist/templates',
            src: ['./{,*/,**/}*.html']
        }]
    }
},

now we can add the steps to version the assets.

usemin: {  
    html: ['target/dist/templates/{,*/}*.html'],
    css: ['target/dist/public/styles/{,*/}*.css'],
    options: {
        dirs: ['target/dist/public']
    }
},
rev: {  
    dist: {
        files: {
            src: [
                'target/dist/public/scripts/{,*/}*.js',
                'target/dist/public/styles/{,*/}*.css',
                'target/dist/public/fonts/{,*/}*.{eot,svg,ttf,woff}',
                'target/dist/public/images/{,*/}*.{jpg,jpeg,gif,png}'
            ]
        }
    }
},

Grunt should now be grating a target/dist directory with our versioned assets and re-written templates. Now we need to add a dist step to our Gradle build. This will include the versioned files and templates into a deployable package.

To do this we need to alter some settings from the application-plugin that Ratpack uses:

def appPluginConvention = project.getConvention().getPlugin(ApplicationPluginConvention)

appPluginConvention.applicationDistribution.from('target/dist/templates') {  
    into "app/templates"
}
appPluginConvention.applicationDistribution.from('target/dist/public') {  
    into "app/public"
}

Development time reloading

Another common task for Grunt builds is a watch task for development time reloading of SASS or CoffeScript compilation. The next section will show a few extra steps to include Grunt watch into Gradle.

task gruntWatch(type: GruntTask) {  
    group = "Build"
    description = "watches for file changes"
    gruntArgs = "watch"
}
task gruntWatcher {  
    doFirst {
        Thread.start {
            gruntWatch.execute()
        }
    }
}
run.dependsOn gruntWatcher  
Comments