Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Friday, June 29, 2012

Gradle CoffeeScript Compilation

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:

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:

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!

1 comment:

Alex said...

The performance of this script could be improved by reusing the CoffeeScriptProcessor instance for all processed resources.