Creator of the Apache Tapestry web application framework and the Apache HiveMind dependency injection container. Howard has been an active member of the Java community since 1997. He specializes in all things Tapestry, including on-site Tapestry training and mentoring, but has lately been spreading out into fun new areas including functional programming (with Clojure), and NodeJS. Howard is a DZone MVB and is not an employee of DZone and has posted 80 posts at DZone. You can read more from them at their website. View Full User Profile

Gradle CoffeeScript Compilation

07.08.2012
| 3200 views |
  • submit to reddit

As I'm working on rebooting JavaScript support in Tapestry one of my goals is to be able to author Tapestry's JavaScript as CoffeeScript. Tapestry will ultimately have a runtime option to dynamically compile CoffeeScript to JavaScript, but I don't want that as a dependency of the core library; that means I need to be able to have that compilation (transpilation?) occur at build time.

Fortunately, this is the kind of thing Gradle does well! My starting point for this is the Web Resource Optimizer for Java project, which includes a lot of code, often leveraging Rhino (for JavaScript) or JRuby, to do a number of common web resource processing, including CoffeeScript to JavaScript. WRO4J has an Ant task, but I wanted to build something more idiomatic to Gradle.

Here's what I've come up with so far. This is a external build script, separate from the project's main build script:

import ro.isdc.wro.model.resource.*
import ro.isdc.wro.extensions.processor.js.*

buildscript {
  repositories { mavenCentral() }
  dependencies {
    classpath "ro.isdc.wro4j:wro4j-extensions:${versions.wro4j}"
  }
}

class CompileCoffeeScript extends DefaultTask {
  def srcDir = "src/main/coffeescript"

  def outputDir = "${project.buildDir}/compiled-coffeescript"

  @InputDirectory
  File getSrcDir() { project.file(srcDir) }

  @OutputDirectory
  File getOutputDir() { project.file(outputDir) }

  @TaskAction
  void doCompile() {
    logger.info "Compiling CoffeeScript sources from $srcDir into $outputDir"

    def outputDirFile = getOutputDir()
    // Recursively delete output directory if it exists
    outputDirFile.deleteDir()

    def tree = project.fileTree srcDir, {
      include '**/*.coffee'
    }

    tree.visit { visit ->
      if (visit.directory) return

      def inputFile = visit.file
      def inputPath = visit.path
      def outputPath = inputPath.replaceAll(/\.coffee$/, '.js')
      def outputFile = new File(outputDirFile, outputPath)

      logger.info "Compiling ${inputPath}"

      outputFile.parentFile.mkdirs()

      def resource = Resource.create(inputFile.absolutePath, ResourceType.JS)
      
      new CoffeeScriptProcessor().process(resource, inputFile.newReader(), outputFile.newWriter())
    }
  }

}

project.ext.CompileCoffeeScript = CompileCoffeeScript

The task finds all .coffee files in the input directory (ignoring everything else) and generates a corresponding .js file in the output directory.

The @InputDirectory and @OutputDirectory annotations allows Gradle to decide when the task's action is needed: If any file changed in the directories provided by these methods then the Task must be rerun. Gradle doesn't tell us exactly what changed, or create the directories, or anything ... that's up to us.

Since I only expect to have a handful of CoffeeScript files, the easiest thing to do was to simply delete the output directory and recompile all CoffeeScript input files on any change. The @TaskAction annotation directs Gradle to invoke the doCompile() method when inputs (or outputs) have changed since the previous build.

It's enlightening to note that the visit passed to the closure on line 34 is, in fact, a FileVisitDetails, which makes it really easy to, among other things, work out the output file based on the relative path from the source directory to the input file.

One of my stumbling points was setting up the classpath to pull in WRO4J and its dependencies; the buildscript configuration on line 4 is specific to this single build script, which is very obvious once you've worked it out. This is actually excellent for re-use, as it means that minimal changes are needed to the build.gradle that makes use of this build script. Earlier I had, incorrectly, assumed that the main build script had to set up the classpath for any external build scripts.

Also note line 54; the CompileCoffeeScript task must be exported from this build script to the project, so that the project can actually make use of it.

The changes to the project's build.gradle build script are satisfyingly small:
apply from: "coffeescript.gradle"


task compileCoffeeScript(type: CompileCoffeeScript)

processResources {
  from compileCoffeeScript
}

The apply from: brings in the CompileCoffeeScript task. We then use that class to define a new task, using the defaults for srcDir and outputDir.

The last part is really interesting: We are taking the existing processResources task and adding a new input directory to it ... but rather than explicitly talk about the directory, we simply supply the task. Gradle will now know to make the compileCoffeeScript task a dependency of the processResources task, and add the task's output directory (remember that @OutputDirectory annotation?) as another source directory for processResources. This means that Gradle will seamlessly rebuild JavaScript files from CoffeeScript files, and those JavaScript files will then be included in the final WAR or JAR (or on the classpath when running test tasks).

Notice the things I didn't have to do: I didn't have to create a plugin, or write any XML, or seed my Gradle extension into a repository, or even come up with a fully qualified name for the extension. I just wrote a file, and referenced it from my main build script. Gradle took care of the rest.

There's room for some improvement here; I suspect I could do a little hacking to change the way compilation errors are reported (right now it is just a big ugly stack trace). I could use a thread pool to do compilation in parallel. I could even back away from the delete-the-output-directory-and-recompile-all approach ... but for the moment, what I have works and is fast enough.

Luke Daly leaked that there will be an experimental CoffeeScript compilation plugin coming in 1.1 so I probably won't waste more cycles on this. For only a couple of hours of research and experimentation I was able to learn a lot, and get something really useful put together, and the lessons I've learned mean that the next one of these I do will be even easier!

  

 

 

Published at DZone with permission of Howard Lewis Ship, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)