This article describes how to automate building Java and JavaScript NPM-based applications within a single Gradle build.
As examples we are going to use a Java backend application based on Spring Boot and a JavaScript frontend application based on React. Though there are no obstacles to replacing them with any similar technologies like DropWizard or Angular, using TypeScript instead of JavaScript, etc.
Our main focus is Gradle build configuration, both applications' details are of minor importance.
We want to serve the JavaScript frontend application as static resources from the Java backend application. The full production package, i.e. a fat JAR containing all the resources, should be automatically created via Gradle.
The NPM project should be built using Gradle, without any direct interaction with npm
or node
CLIs. Going further, it should not be necessary to have them installed on the system at all - especially important when building on a CI server.
The Java project is built with Gradle in a regular way, no fancy things here.
The NPM build is done using gradle-node-plugin, which integrates NodeJS-based projects with Gradle without requiring to have NodeJS installed on the system.
Output of the NPM build is packaged into a JAR file and added as a regular dependency to the Java project.
During work on this article an actively developed fork of gradle-node-plugin has appeared. It's a good news since the original plugin seemed abandoned. However, due to the early phase of the fork development, we decided to stick with the original plugin, eventually upgrading in the future.
Create a root Gradle project, lets call it java-npm-gradle-integration-example
, then java-app
and npm-app
as it's subprojects.
Create java-npm-gradle-integration-example
Gradle project with the following configuration.
java-npm-gradle-integration-example/build.gradle
defaultTasks 'build'
wrapper {
description "Regenerates the Gradle Wrapper files"
gradleVersion = '5.0'
distributionUrl = "http://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip"
}
java-npm-gradle-integration-example/settings.gradle
rootProject.name = 'java-npm-gradle-integration-example'
The directory structure is expected to be as below:
java-npm-gradle-integration-example/
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
Generate a Spring Boot application using Spring Initializr, with Web
dependency and Gradle as build type. Place the generated project under java-npm-gradle-integration-example
directory.
Generate npm-app
React application using create-react-app under java-npm-gradle-integration-example
directory.
Remove gradle
directory, gradlew
, gradlew.bat
and settings.gradle
files from java-app
as they are provided by the root project.
Update the root project to include java-app
by adding the following line
include 'java-app'
to java-npm-gradle-integration-example/settings.gradle
.
Now building the root project, i.e. running ./gradlew
inside java-npm-gradle-integration-example
directory should build the java-app
as well.
This is the essential part consisting of converting npm-app
to Gradle subproject and executing npm build via Gradle script.
Create npm-app/build.gradle
file with the following contents, already including gradle-node-plugin dependency.
buildscript {
repositories {
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0'
}
}
apply plugin: 'base'
apply plugin: 'com.moowork.node' // gradle-node-plugin
Below add configuration for gradle-node-plugin declaring the versions of npm/NodeJS to be used. The download
flag is crucial here as it decides about downloading npm/NodeJS by the plugin or using the ones installed in the system.
node {
/* gradle-node-plugin configuration
https://github.com/srs/gradle-node-plugin/blob/master/docs/node.md
Task name pattern:
./gradlew npm_<command> Executes an NPM command.
*/
// Version of node to use.
version = '10.14.1'
// Version of npm to use.
npmVersion = '6.4.1'
// If true, it will download node using above parameters.
// If false, it will try to use globally installed node.
download = true
}
Now it's time to configure the build task. Normally the build would be done via npm run build
command. gradle-node-plugin allows executing npm commands using the following underscore notation: /gradlew npm_<command>
. Behind the scenes it dynamically generates a Gradle task. So for our purpose the Gradle task is npm_run_build
.
Let's customize its behavior - we want to be sure it is executed only when the appropriate files change and avoid any unnecessary building. In order to do so we define inputs
and outputs
pointing files or directories to be monitored for changes between executions of the task. Not to be confused with specifying files the task consumes or produces. In case a change is detected the task is going to be executed otherwise it will be treated as up-to-date and skipped.
npm_run_build {
inputs.files fileTree("public")
inputs.files fileTree("src")
inputs.file 'package.json'
inputs.file 'package-lock.json'
outputs.dir 'build'
}
One would say we are missing node_modules
as inputs here, though this directory appeared not reliable for dependency change detection. The task was rerun without changes, probably enormous number of node_modules files does not help here either. Instead we monitor only package.json
and package-lock.json
as they reflect state of dependencies enough.
Finally make the Gradle build depend on executing npm build:
assemble.dependsOn npm_run_build
Now include npm-app
in the root project by adding the following line to java-npm-gradle-integration-example/settings.gradle
:
include 'npm-app'
At this moment you should be able to build the root project and see the npm build results under npm-app/build
directory.
Now we need to somehow put the npm build result into a Java package. We would like to do it without awkward copying external files into Java project resources during the build. Much more elegant and reliable way is to add them as a regular dependency, just like any other library.
Let's update npm-app/build.gradle
to achieve this.
At first define task packing results of the build into JAR file:
task packageNpmApp(type: Zip) {
dependsOn npm_run_build
baseName 'npm-app'
extension 'jar'
destinationDir file("${projectDir}/build_packageNpmApp")
from('build') {
// optional path under which output will be visible in Java classpath, e.g. static resources path
into 'static'
}
}
Now we need to define a custom configuration to be used for publishing the JAR artifact:
configurations {
npmResources
}
configurations.default.extendsFrom(configurations.npmResources)
We do not use here any predefined configurations, like archives
, in order to be sure no other dependencies are included in the published scope.
Then expose the artifact created by the packaging task:
artifacts {
npmResources(packageNpmApp.archivePath) {
builtBy packageNpmApp
type "jar"
}
}
where archivePath
points the created JAR file.
Next make the build depend on packageNpmApp
task rather than the directly on the build task by replacing line
assemble.dependsOn npm_run_build
with
assemble.dependsOn packageNpmApp
Don't forget to configure proper cleaning as now the output doesn't go to the standard Gradle build directory:
clean {
delete packageNpmApp.archivePath
}
Finally, include npm-app
project as a dependency of java-app
by adding
runtimeOnly project(':npm-app')
to the dependencies { }
block of java-app/build.gradle
. Here the scope (configuration) is runtimeOnly
since we do not want to include the dependency during compilation time.
Now executing the root project build, i.e. inside java-npm-gradle-integration-example
running a single command
./gradlew
should result in creating java-app
JAR containing,
apart of the java project's classes and resources, also the npm-app
bundle packaged into JAR.
In our case the mentioned npm-app.jar
resides in java-app/build/libs/java-app-0.0.1-SNAPSHOT.jar
:
zipinfo -1 java-app/build/libs/java-app-0.0.1-SNAPSHOT.jar
...
BOOT-INF/classes/eu/xword/labs/gc/JavaAppApplication.class
BOOT-INF/classes/application.properties
BOOT-INF/lib/
BOOT-INF/lib/spring-boot-starter-web-2.1.1.RELEASE.jar
BOOT-INF/lib/npm-app.jar
BOOT-INF/lib/spring-boot-starter-json-2.1.1.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-2.1.1.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-tomcat-2.1.1.RELEASE.jar
...
Last but not the least - check if all of this works. Start the Java application with the following command:
java -jar java-app/build/libs/java-app-0.0.1-SNAPSHOT.jar
and open http://localhost:8080/
in your browser. You should see the React app welcome page.
The Java tests are handled in standard way by the java plugin, no changes here.
In order to run JavaScript tests during the Gradle build we need to create a task that would execute npm run test
command.
Here it's important to make sure the process started by such task exits with a proper status code, i.e. 0
for success
and non-0
for failure - we don't want our Gradle build pass smoothly ignoring JavaScript tests blowing up.
In our example it's enough to set CI
environment variable - the Jest
testing platform (default for create-react-app)
is going to behave correctly.
String testsExecutedMarkerName = "${projectDir}/.tests.executed"
task test(type: NpmTask) {
dependsOn assemble
// force Jest test runner to execute tests once and finish the process instead of starting watch mode
environment CI: 'true'
args = ['run', 'test']
inputs.files fileTree('src')
inputs.file 'package.json'
inputs.file 'package-lock.json'
// allows easy triggering re-tests
doLast {
new File(testsExecutedMarkerName).text = 'delete this file to force re-execution JavaScript tests'
}
outputs.file testsExecutedMarkerName
}
We also add a file marker for making re-execution of tests easier.
Finally make the project depend on tests execution
check.dependsOn test
And update clean
task:
clean {
delete packageNpmApp.archivePath
delete testsExecutedMarkerName
}
That's it. Now your build includes both Java and JavaScript tests execution.
In order to execute the latter individually just run ./gradlew npm-app:test
.
We integrated building Java and JavaScript/NPM projects into a single Gradle project.
The Java project is build in a standard manner, whereas the JavaScript one is build by npm
tool wrapped with Gradle script
using gradle-node-plugin
. The plugin can provide npm
and node
so they do not need to be installed on the system.
Result of the build is a standard Java package (fat JAR) additionally including JavaScript package as classpath resource to be served as a static asset.
Such setup can be useful for simple frontend-backend stacks when there is no need to serve frontend application from a separate server.
Full implementation of this example can be found on GitHub.