Sunday, November 4, 2012

Integrating Google Closure Tools with Maven Builds

We wanted to use Google Closure Tools (compiler, plus the library) in our Maven-based project. The current approaches posted here and here were very good. But didn't quite satisfy our needs. As our JavaScript code base grew in size, and in complexity, we needed to run/debug our web pages with the JavaScript code in their raw form, apart from the minified/compiled mode. So, here's how we did it.

Project Structure (Maven's Standard Directory Layout)

Our project structure is as follows:

  • src/main/java
  • src/main/js
  • src/main/js/closure-library
  • src/main/js/myapp
  • src/main/resources
  • src/test/java
  • src/test/resources
  • src/main/webapp

It follows the standard directory layout. We placed our JavaScript (JS) code under src/main/js. We also placed the Closure Library underneath it. This made it easy to expose as additional web app root directories via the Jetty Maven Plugin. As will be explained later, this was crucial in being able to run the our JS code in raw form, which required the inclusion of base.js and our app's deps.js.

In our Subversion code repository, we linked to the Closure Library's code repository. A similar approach can be applied using git-svn mirror when using Git.

$ svn propget svn:externals
src/main/js/closure-library
http://closure-library.googlecode.com/svn/trunk/

This external link provided src/main/js/closure-library/closure/goog/base.js to be part of our project structure.

Running/Debugging with Closure JavaScript in Raw Form

To have quick code-run-debug cycles, we couldn't afford to compile the JS code every time. So, we had to find a way to run our web pages with the following JS script tags.

<html>
<head>...</head>
<body>
<script src="closure-library/closure/goog/base.js"></script>
<script src="myapp/deps.js"></script>
<script src="myapp/main.js"></script>
<script>init();</script><!-- our JS code's entry point -->
</body>
</html>

To make this possible, we needed to expose the JS files on our servlet container. This was made easier by having our JS code and Closure Library under one folder.

<plugin>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>jetty-maven-plugin</artifactId>
  <configuration>
    <webAppConfig>
      <contextPath>/</contextPath>
      <baseResource 
          implementation="org.eclipse.jetty.util.resource.ResourceCollection">
        <resourcesAsCSV>src/main/webapp,src/main/js</resourcesAsCSV>
      </baseResource>
    </webAppConfig>
  </configuration>
</plugin>

Calculating Dependencies

Our JS code was not just one file. It had more than a dozen files. Just like the ones in the Closure Library, each class was placed in one file. Each had goog.provide and goog.require calls. For our JS classes to work, we needed to generate a dependency file. The Closure Library comes with its pre-generated deps.js file for its classes. We'll need one for our classes. We used the Python script (that comes with the Closure Library), closure/bin/build/depswriter.py. We chose to invoke it during the process-sources phase of the build. This made it easier when running the jetty:run command, as the dependency file will be re-generated before the servlet container is up and running.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>calculate-dependencies</id>
      <phase>process-sources</phase>
      <goals>
        <goal>exec</goal>
      </goals>
      <configuration>
        <executable>python</executable>
        <arguments>
          <argument>src/main/js/closure-library/closure/bin/build/depswriter.py</argument>
          <argument>--root_with_prefix</argument>
          <!-- prefix is relative to location of closure-library/closure/goog/base.js -->
          <argument>src/main/js/myapp/ ../../../myapp/</argument>
          <argument>--output_file</argument>
          <argument>src/main/js/maypp/deps.js</argument>
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

If there are no changes to the dependencies, you can modify your JS code, and just refresh/reload the web page, and the JS code is also reloaded. This provides a very fast and convenient code-run-debug cycle.

The resulting HTML should have loaded the JS files in the correct order. It should look something like this when viewed using your browser's debug mode (e.g. Firebug, or Webkit's Web Inspector).

<script src="/closure-library/closure/goog/base.js"></script>
<script type="text/javascript" src="http://localhost:8080/closure-library/closure/goog/deps.js"></script>
<script src="/myapp/deps.js"></script>
<script src="/myapp/main.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/disposable/idisposable.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/disposable/disposable.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/debug/error.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/string/string.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/asserts/asserts.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/array/array.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/../../../myapp/finder.js"></script>
<script type="text/javascript"
  src="http://localhost:8080/closure-library/closure/goog/../../../myapp/model.js"></script>
<script type="text/javascript">init();</script><!-- our JS code's entry point -->

It's interesting to point out how the Closure Library adds its deps.js file, and how it adds the dependencies (e.g. idisposable.js, error.js, string.js). The dependencies are due to the JS code requiring these, and the deps.js file adds in the corresponding JS <script> tags. Also note how our app's JS files are referenced — relative to the location of base.js. Look at lines 17-20. This is why we had to use the --root_with_prefix argument when calling depswriter.py. Otherwise, our JS code wouldn't even be loaded.

I'll post another follow-up on this topic where I'll explain how we compile, minify, and test the minified JS code.

No comments:

Post a Comment