Wednesday, November 7, 2012

Google Closure Compiler with Maven Builds

In my previous post, I showed how we used Closure Library with our Maven-based project. More specifically, we used the Jetty Maven plugin to expose the JS code, and make it available in raw form for a fast and convenient code-run-debug cycle. This time, let me show how we used the Closure Compiler to compile and generate the minified JS file, and how we used it in our builds.

Running Closure Compiler

Running the compiler was a bit tricky. Since we didn't have just one single JS file, we needed to provide the compiler a list of JS files in the correct order (where providing classes are declared before the ones that required them). Good thing the library comes with Python scripts that does just that.

<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/myapp/deps.js</argument>
    </arguments>
   </configuration>
  </execution>
  <execution>
   <id>compile-javascript</id>
   <phase>prepare-package</phase>
   <goals>
    <goal>exec</goal>
   </goals>
   <configuration>
    <executable>python</executable>
    <arguments>
     <argument>src/main/js/closure-library/closure/bin/build/closurebuilder.py</argument>
     <argument>--root</argument>
     <argument>src/main/js/closure-library</argument>
     <argument>--root</argument>
     <argument>src/main/js/myapp</argument>
     <argument>--output_mode</argument>
     <argument>compiled</argument>
     <argument>--compiler_jar</argument>
     <argument>src/lib/closure-compiler-20120430.jar</argument>
     <!-- WHITESPACE_ONLY, SIMPLE_OPTIMIZATIONS, ADVANCED_OPTIMIZATIONS -->
     <argument>--compiler_flags=--compilation_level=ADVANCED_OPTIMIZATIONS</argument>
     <!-- QUIET, DEFAULT, VERBOSE -->
     <argument>--compiler_flags=--warning_level=VERBOSE</argument>
     <argument>--namespace</argument>
     <argument>myapp</argument>
     <argument>--output_file</argument>
     <argument>${project.build.directory}/replaced-webapp/scripts/myapp.min.js</argument>
    </arguments>
   </configuration>
  </execution>
 </executions>
</plugin>

I've included the portion that calculates dependencies (from my previous post). Note that use of src/lib/closure-compiler-yyyymmdd.jar. You can download it from here, and extract the compiler.jar file. We just renamed it so we can track which version we're using. And in case of updates, just update the pom.xml.

Note also that we invoke the compiler during the prepare-package phase. This means that when you run jetty:run, the compiler does not get invoked. This makes it faster (and easier to repeat). But when you need to run/debug with the compiled/minified form (with advanced optimization), you run jetty:run-war.

The astute reader would notice that the minified file is currently not being referenced in any web page. This shall be explained in the next section.

Replacing Tags in Web Pages

Since the output of the compiler was one single JS file, we needed to modify our web pages (e.g. JSPs) to refer to it (and not the multiple files). Obviously, we could manually modify the JSP files. But we needed something that would automatically replace them during the build. We decided to use maven-replacer-plugin.

<plugin>
  <groupId>com.google.code.maven-replacer-plugin</groupId>
  <artifactId>maven-replacer-plugin</artifactId>
  <version>1.4.1</version>
  <executions>
    <execution>
      <phase>prepare-package</phase>
      <goals>
        <goal>replace</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <ignoreMissingFile>true</ignoreMissingFile>
    <file>src/main/webapp/index.jsp</file>
    <outputFile>${project.build.directory}/replaced-webapp/index.jsp</outputFile>
    <regex>true</regex>
    <regexFlags>
      <regexFlag>MULTILINE</regexFlag>
      <regexFlag>DOTALL</regexFlag>
    </regexFlags>
    <replacements>
      <replacement>
        <token><!-- SCRIPTS.*END SCRIPTS --></token>
        <value><script src="<c:url value='/scripts/myapp.min.js' />" ></script></value>
      </replacement>
    </replacements>
  </configuration>
</plugin>

This effectively replaces the following:

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

with

<script src="<c:url value='/scripts/myapp.min.js' />"></script>
<script>init();</script><!-- our JS code's entry point -->

Note that the minified JS file is written to target/replaced-webapp/scripts. But this folder is never used to build the WAR. So, we need to modify our pom.xml to include this folder when building the WAR file.

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-war-plugin</artifactId>
 <configuration>
  <webResources>
   <resource>
    <directory>src/main/webapp</directory>
    <filtering>true</filtering>
   </resource>
   <resource>
    <directory>target/replaced-webapp</directory>
    <filtering>true</filtering>
    <includes>
     <include>**/*.jsp</include>
    </includes>
   </resource>
  </webResources>
 </configuration>
</plugin>

The above configuration effectively adds the target/replaced-webapp directory on to the WAR file. This adds a scripts folder with a minified JS file in it. It also contains the modified index.jsp file. When the web page references /scripts/myapp.min.js, it will find it.

Conclusion

It is great that we're able to debug in raw form. This provides an easier way to find which JS file/line is causing the error. We are also able to debug in minified form, and deploy the minified form. I hope this helps.

1 comment:

  1. Just wanted to say thanks for these 2 articles. We're not using Maven (or java) but I am trying to figure out how best to structure our Google Closure project, and this post helps.

    ReplyDelete