Home | Send Feedback

Integrate Parcel into a Maven project

Published: 13. January 2018  •  Updated: 31. October 2021  •  java, javascript

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"
  },

package.json

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");
  }
}

ResourceConfig.java

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

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>

pom.xml

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>

pom.xml


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>

pom.xml

The name of the jar file is defined with the finalName configuration

      <plugin>

pom.xml


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