Tuesday 15 April 2014

Grunt + Bower + Maven

iTeach is a web application that helps independent teachers to organise their lessons and their agenda. It is composed of two main elements:
  • an API service
  • a GUI layer that calls this API
Although the API service is a standard layered Spring MVC application, built and assembled using Maven, the GUI layer is an AngularJS one-page application, that relies on Grunt. It is however obvious that those two modules are two parts of the same application, and I needed a way to integrate seamlessly the Grunt module into a larger Maven structure.

The application is structured this way:

iteach-parent
    |-- pom.xml              // The parent POM
    |-- iteach-ui            // The WAR that serves the API
    |      |-- pom.xml
    |-- iteach-gui-static    // The GUI layer
    |      |-- pom.xml       // The GUI module is still considered a Maven module
    |      |-- Gruntfile.js  // The Grunt build file, plus other files
    |-- ... other modules

At the top level, the iteach-gui-static module is still considered a normal Maven module and is registered as such in the parent POM:

<modules>
        ...
        <module>iteach-ui</module>
        ...
        <module>iteach-gui-static</module>
</modules>

The POM file for the iteach-gui-static module uses extensively the exec-maven-plugin in order to call npm, bower and grunt in the correct phases:

<build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>prepare-env-npm</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>install</argument>
                            </arguments>
                        </configuration>
                    </execution>
                    <execution>
                        <id>prepare-env-bower</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>bower</executable>
                            <arguments>
                                <argument>install</argument>
                                <argument>--force-latest</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <profiles>

        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>exec-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>prepare-dev</id>
                                <phase>prepare-package</phase>
                                <goals>
                                    <goal>exec</goal>
                                </goals>
                                <configuration>
                                    <executable>grunt</executable>
                                    <arguments>
                                        <argument>clean</argument>
                                        <argument>dev</argument>
                                    </arguments>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>

        <profile>
            <id>release</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-antrun-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>prepare-release-version</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>run</goal>
                                </goals>
                                <configuration>
                                    <target name="replaceVersion">
                                        <echo>Replacing versions in *.json files</echo>
                                        <replaceregexp
                                                match=""version": "(.*)""
                                                replace=""version": "${project.version}""
                                                byline="true"
                                                encoding="UTF-8"
                                                >
                                            <fileset dir="${basedir}" includes="*.json" />
                                        </replaceregexp>
                                    </target>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>exec-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>prepare-release</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>exec</goal>
                                </goals>
                                <configuration>
                                    <executable>grunt</executable>
                                    <arguments>
                                        <argument>clean</argument>
                                        <argument>prod</argument>
                                    </arguments>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>

    </profiles>


You can see the complete source code on GitHub.

In the validate phase, I run the following commands:
  • npm install
  • bower install --force-latest
In dev profile, during the prepare-package phase, I run the grunt clean dev command, that makes all GUI files ready in the local target/dev folder (important, see later).

In release profile, during the prepare-package phase, I do two things:
  • replacing the version in the bower.json and the package.json files by the version to release
  • run the grunt clean prod command, which packages and compresses all files in target/prod

Now, back in the iteach-ui module (the part that defines the actual WAR to be deployed), I introduce iteach-gui-static as a runtime dependency, in order to force the WAR to be built after the GUI layer has been prepared:

        <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>iteach-gui-static</artifactId>
        <version>${project.version}</version>
        <scope>runtime</scope>
   </dependency>

For the release profile, I just tell Maven to copy the target/prod folder of the iteach-gui-static module just before the packaging:

<profile>
  <id>release</id>
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-resources-plugin</artifactId>
          <executions>
            <execution>
              <id>copy-gui-resources</id>
              <phase>compile</phase>
              <goals>
                <goal>copy-resources</goal>
              </goals>
              <configuration>
                <outputDirectory>src/main/webapp</outputDirectory>
                <resources>
                  <resource>
                    <directory>../iteach-gui-static/target/prod</directory>
                  </resource>
                </resources>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</profile>

That's it for the release: the packaged WAR will embed the compressed static resources.

For the development mode, I'm using the tomcat7-maven-plugin and I tell Tomcat to use the target/dev folder of the iteach-gui-static module as the root of static documents:

<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
    <path>/</path>
    <contextFile>src/test/resources/tomcat/context-dev.xml</contextFile>
    <warSourceDirectory>../iteach-gui-static/target/dev</warSourceDirectory>
    ...
</configuration>

Here we are.

So, to summarise my set-up:

  • a normal AngularJS + Bower + Grunt application declared as a Maven module, with the correct Grunt calls mapped to Maven phases
  • a dependency from the WAR to the GUI layer, with a copy of the static resources
When I develop, in a shell or in my Intellij terminal view, I just type grunt watch in the iteach-gui-static folder and launch my Web app using tomcat7:run from within my IDE. The grunt watch command launches a Livereload server that allows my browser to refresh automatically whenever I change one of my static resources. Neat.

At the end, when I release, I just have to rely on mvn package. The necessary NPM + Bower + Grunt calls will be done.

For the complete source code, go to GitHub.

No comments:

Post a Comment