NPM is the standard to orchestrate a javascript build, even delegating front dependencies to bower or the build itself to gulp or webpack, the starting point is often npm.

Where you easily find java (maven/gradle) plugin to build a frontend project, the opposite is rare. This is probably justified by the fact you often embed a single page application (SPA) in a java application but rarely the opposite....normally.

A SPA is after all just a static website so nothing should prevent it to be served from a Servlet container. What would be the interest?

  • you already have a servlet container you control but you don't control a front (not there or just there as proxy)
  • colocalizing the SPA with another application to save memory or process to manage
  • reuse an established stack ("deploy in tomcat")
  • reuse an existing tooling around war deployment or monitoring you don't have yet around nodejs deployments (got this case for cluster deployment for instance)
  • ...

How to do it?

The easiest way is to build it through a script npm can launch. A war is after all just a zip with some specific files, right? Concretely you need to follow these high level steps:

  1. build your application (webpack, gulp, ... are good candidates)
  2. create a zip containing as root resources the SPA (web) resources
  3. (optional) map some urls in web.xml and add the web.xml in WEB-INF of the archive (more about this step in next part)

Dependencies

From now on I'll only deal with parts 2 and 3 cause 1 is highly dependent on your application and 100% related to your javascript build.

To create a zip we'll use archiver dependencies of npm:

{
  "name": "warjs",
  "version": "1.0",
  "author": "You",
  "license": "MIT",
  "description": "Build a war from npm",
  "repository": "https://github.com/you/warjs.git",
  "scripts": {
  },
  "dependencies": {
  },
  "devDependencies": {
    "archiver": "1.2.0"
  }
}

War packaging

To simulate a packaging phase (reference to maven) we'll add a script to npm called bundle intended to create our war:

{
  "name": "warjs",
  "version": "1.0",
  "author": "You",
  "license": "MIT",
  "description": "Build a war from npm",
  "repository": "https://github.com/you/warjs.git",
  "scripts": {
    "bundle": "rimraf dist && webpack --config  webpack.config.js && node bundle.ts",
  },
  "dependencies": {
  },
  "devDependencies": {
    "archiver": "1.2.0"
  }
}

This bundle script will:

  1. cleanup our dist directory (~ target for maven)
  2. build our frontend application (using webpack in this example but any build would fit)
  3. finally run bundle.ts script

The bundle.ts is responsible to create the war from the previously built resources. Here a light version:

const fs = require('fs');
const archiver = require('archiver');

// 1
const out = 'dist/application.war';

// 2
const output = fs.createWriteStream(out);
const archive = archiver('zip', {});

// 3
output.on('finish', () => {
    console.log('war (' + out + ') ' + archive.pointer() + ' total bytes');
});

// 4
archive.pipe(output);

// 5
archive.bulk([{ expand: true, cwd: 'dist/application', src: ['**'], dest: '/'}]);

// 6
archive.finalize();
  1. we want to build our war in dist/application.war

  2. we create an output stream for our war and an archive handler (archive)

  3. we log some message when the war is done (never forget to let the user know you did something in a script)

  4. we redirect the archive content to the output stream of our war

  5. we add to the archive (so the war) all the context of dist/application as root web resources of the war (dest)

  6. we close the zip

HTML5 routing and war?

This solution works fine but one case is not handled: html5 routing. This solution consists to replace the hash routing by a more natural routing without hash. Typically you'll get this:

# standard hash routing
/myapp/#/page1
/myapp/#/page2

# html5 routing
/myapp/page1
/myapp/page2

Often node servers have an option to redirect all pages not matching any resource (js, css) to index.html which will handle the routing. In a servlet container it is more complicated cause all these urls will return a 404 so links will not be shareable and users would need to start from the home page everytime.

To solve it in a java application you can write a filter - or reuse an existing one - redirecting all client urls (page1, page2 in previous example) to the index.html page but without writing java code you need to use the web.xml.

Using string literals of javascript it is made a bit easier. Once built the web.xml just needs to be added to the archive in WEB-INF/web.xml:

const resources = ['*.eot', '*.svg', '*.woff', '*.woff2', '*.ttf', '*.js', '*.js.map', '*.png', '*.jpg'];
const html5routes = ['/', '/page1', '/page2'];

archive.append(`<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
          http://java.sun.com/xml/ns/javaee
          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0" metadata-complete="true">
    <!-- we define index.html as a servlet to be able to reference it -->
    <servlet>
        <servlet-name>index</servlet-name>
        <jsp-file>/index.html</jsp-file>
    </servlet>

    <!-- we map all existing resources to the default servlet ("file" one) -->
` +
    resources.map(m =>
    `   <servlet-mapping>
          <servlet-name>default</servlet-name>
          <url-pattern>` + m + `</url-pattern>
        </servlet-mapping>
`).join('\n') +
    '\n   <!-- html5 routing mappings -->\n' +
    html5routes.map(m =>
`   <servlet-mapping>
      <servlet-name>index</servlet-name>
      <url-pattern>` + m + `</url-pattern>
    </servlet-mapping>
`).join('\n') + `
</web-app>`, { name: 'WEB-INF/web.xml' });

// finalize is still the last line
archive.finalize();

With this web.xml the application will still behave correctly even in a servlet container. The only drawback is to maintain the resources and htmlroutes variables.

Last tip

Before concluding that post, keep in mind servlet container will often deploy the war in a context (sub path in the url). For a SPA developped to be deployed on root you should then either ensure it is named ROOT.war for Tomcat/TomEE/...or ensure you adapt the base in your index.html. If you don't do it then the application links and resources will not behave as expected.

Conclusion

Even if not that natural, creating a war from npm is very possible. The companion steps can be:

  • push the war to nexus (just a POST request to do ;))
  • instead of creating a war you can build an embeddable jar user can add to their war and embed your application in their own one!
  • alternative to this full process is to add a pom.xml (or build.gradle) and just use frontend-maven-plugin to orchestrate the full build.

From the same author:

In the same category: