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.