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>

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.