Integrating Java and npm Builds Using Gradle - Groovy or Kotlin DSL
We take a look at building a Java-based app that serves up a JavaScript/npm-based app as a static resource, all using Gradle.
Join the DZone community and get the full member experience.
Join For FreeThis 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 front-end 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.
UPDATE: Gradle DSL, Groovy or Kotlin - the choice is yours
The article uses Groovy DSL for Gradle scripts, though this approach works perfectly fine with Kotlin DSL as well.
Both versions, Groovy and Kotlin DSL, of the whole working example can be found on GitHub.
Goal
We want to serve the JavaScript front-end 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 Plan
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 Node.js-based projects with Gradle without requiring to have Node.js 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.
Digression - gradle-node-plugin
During work on this article an actively developed fork of gradle-node-plugin has appeared. It's 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, and eventually upgrade in the future.
Initial Setup
Create a root Gradle project, let's call it java-npm-gradle-integration-example
, then java-app
and npm-app
as its subprojects.
Create the Root Project
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
Create a java-app
Project
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.
Create an npm-app
Project
Generate npm-app
React application using create-react-app under java-npm-gradle-integration-example
directory.
Adapt java-app
to be a Gradle Subproject of java-npm-gradle-integration-example
Remove the 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.
Make npm-app
Be Built by Gradle
This is the essential part consisting of converting npm-app
to a Gradle subproject and executing the npm build via a Gradle script.
Create an npm-app/build.gradle
file with the following content, already including the 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, we have added the configuration for gradle-node-plugin, declaring the versions of npm/Node.js to be used. The download
flag is crucial here as it decides whether to download npm/Node.js via the plugin or by 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 the npm run build
command. gradle-node-plugin allows for the execution of npm commands using the following underscore notation: /gradlew npm_<command>
. Behind the scenes, it dynamically generates a Gradle task. So, for our purposes, 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 thenpm-app/build
directory.
Pack npm Build Result Into a JAR and Expose to the Java Project
Now we need to somehow put the npm build result into a Java package. We would like to do it without awkwardly copying external files into Java project resources during the build. A 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 the task and pack the results of the build into a 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, the npm-app
bundle packaged into a JAR.
In our case, the 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 to see that 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.
What About Tests?
The Java tests are handled in a 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 thenpm run test
command.
Here it's important to make sure the process started by such tasks exits with a proper status code, i.e. 0
for success and non-0
for failure — we don't want our Gradle build to pass smoothly, ignoring JavaScript tests that are blowing up. In our example, it's enough to set a 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 the 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
.
Summary
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.
The result of the build is a standard Java package (fat JAR), additionally including a JavaScript package as classpath resource to be served as a static asset.
Such a setup can be useful for simple front-end/backend stacks when there is no need to serve front-end applications from a separate server.
Full implementations in both, Groovy and Kotlin DSL, of this example can be found on GitHub.
Opinions expressed by DZone contributors are their own.
Comments