In this post, I will show you how to integrate Parcel into a Maven project and create a Spring Boot executable jar containing the back- and front-end code.
The Spring Boot / Maven project is based on the starter application generated with the https://start.spring.io website.
I use src/main/frontend
as the project directory for the JavaScript / CSS / HTML code, but you can use any directory.
In there, I create the file package.json
, set up Parcel, and add a couple of tasks.
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"build": "parcel build",
"start": "parcel",
"watch": "parcel watch"
},
npm run build
builds the production package and writes the output into src/main/frontend/dist
.
npm run start
starts Parcel in watch mode and starts an HTTP server.
npm run watch
starts the directory watcher without the HTTP server. Parcel still supports hot code reloading in the browser in this mode.
Development ¶
During development, I start the Spring Boot server inside the IDE by starting the main application class (with the development
profile) or in the shell with mvnw spring-boot:run -Dspring-boot.run.profiles=development
.
And in another shell, I start Parcel with npm run watch
. This does not start the webserver in Parcel, because I want to serve the front end application through the embedded HTTP server in Spring Boot.
The advantage of this is that I don't need to enable CORS on the endpoints, just for development, and the code can reference the Spring Boot endpoints with relative paths.
And thanks to the watch mode from Parcel, every time I change something in the JavaScript code, it automatically reloads the change into the browser.
Here is the configuration class that serves all requests (/**
) from the src/main/frontend/dist
directory when the development profile is activated.
package ch.rasc.demo.demo;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.EncodedResourceResolver;
@Configuration
class ResourceConfig implements WebMvcConfigurer {
@Autowired
private Environment environment;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (this.environment.acceptsProfiles(Profiles.of("development"))) {
String userDir = System.getProperty("user.dir");
registry.addResourceHandler("/**")
.addResourceLocations(
Paths.get(userDir, "src/main/frontend/dist").toUri().toString())
.setCachePeriod(0);
}
else {
registry.addResourceHandler("/", "/index.html")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.noCache()).resourceChain(false)
.addResolver(new EncodedResourceResolver());
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
.resourceChain(false).addResolver(new EncodedResourceResolver());
}
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/index.html");
}
}
When the default profile is activated (production
), the configuration runs the else clause and sets the web root to the /static folder in the classpath. index.html is served with caching disabled, the other resources are, thanks to Parcel revisioned so that the HTTP server can serve them with a cache expiry date set one year in the future.
Production ¶
The packaging process for the production needs to
- compile the Java code
- build the front end with parcel build (
npm run build
) - copy the output from
src/main/frontend/dist
intotarget/static
- package everything together into a jar file
For these tasks, we use the spring-boot-maven-plugin and the frontend-maven-plugin Maven plugins.
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>build</id>
<goals>
<goal>npm</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
<configuration>
<nodeVersion>v20.10.0</nodeVersion>
<workingDirectory>src/main/frontend</workingDirectory>
<installDirectory>${basedir}/install</installDirectory>
<environmentVariables>
<NODE_ENV>production</NODE_ENV>
</environmentVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>prepare-package</phase>
frontend-maven-plugin downloads and installs Node.js locally into the project. I specified a node version, so the plugin always builds the application with the same Node.js version regardless of what version is globally installed. Another benefit is that I can build the application on any computer without installing Node.js globally.
There are three executions configured. The first installs node and npm, the second runs npm install
, and the third runs npm run build
during the prepare-package phase. This is the task that calls the Parcel build process.
Because the output from the Parcel build needs to end up in the final jar, Maven has to copy the contents of the src/main/frontend/dist
folder to the target folder.
For this purpose, I added the maven-antrun-plugin with a copy task.
<target>
<copy todir="${basedir}/target/classes/static">
<fileset dir="${basedir}/src/main/frontend/dist">
<include name="**" />
</fileset>
</copy>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
The spring-boot-maven-plugin runs after the copy step and packages everything together into one jar file.
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<executions>
<execution>
The name of the jar file is defined with the finalName configuration
<plugin>
To start the production package, I can now run this command.
java -jar target/demo.jar
and open a browser with the URL http://localhost
You find the complete project on GitHub: https://github.com/ralscha/blog/tree/master/parcelmaven