Blog

mvnpm: npm packages as Maven dependencies, without the wait

This post is based on my part of Quarkus Insights #139: Quarkus Web Bundler, streamed on 25 September 2023. The mvnpm section starts at about 5:40.

mvnpm is a Maven repository that sits in front of the npm registry. You ask Maven for an npm package and mvnpm turns it into a jar. The source is on GitHub. It started because I needed it for the Quarkus Dev UI, and it is now also used by the Quarkus Web Bundler, but it is not Quarkus specific. Anywhere you use WebJars today, you should be able to use mvnpm.

Why not just use WebJars

WebJars is a great project and I used it for years. It lets you manage client-side dependencies with Maven or Gradle, pulls in transitive dependencies and is deployed to Maven Central. All of that is still true for mvnpm. It is really a new WebJars, written in Java and Quarkus.

A few things bothered me about WebJars:

  • It pulls in unstable dependencies. If a library depends on a range and the next alpha is out, you get the alpha. mvnpm stays on released versions.
  • Updates are manual. You go to the website, click update, click deploy, and wait for Central.
  • The jars contain the source. mvnpm filters that out, so Lit 2.4.0 is about half the size.
  • The naming is odd. The group id carries the type (npm, bower, classic) and the artifact id carries the namespace and the project. mvnpm maps the npm namespace to the group id.
  • It is written in Scala, and libraries already in Central cannot be changed anyway.

How it works

Add mvnpm as a repository in your settings.xml, after Central:

<repository>
    <id>mvnpm.org</id>
    <url>https://repo.mvnpm.org/maven2</url>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
</repository>

Then add the package you want as a normal dependency. The group id is org.mvnpm, and a scoped package like @namespace/name becomes org.mvnpm.at.namespace:name.

<dependency>
    <groupId>org.mvnpm</groupId>
    <artifactId>lit</artifactId>
    <version>2.8.0</version>
</dependency>

Gradle works the same way. Add https://repo.mvnpm.org/maven2 as a Maven repository and use org.mvnpm:lit:2.8.0.

When you ask for a version that is not in Central yet, Central returns a 404 and Maven moves on to mvnpm. If mvnpm does not have it either, it downloads the tar file from the npm registry, builds just the pom and the jar so your build can continue, and returns them straight away. In the background it then creates the javadoc, sources and signatures and syncs the package to Central. That takes a few minutes per file.

By the time your CI runs, the package should already be in Central, so CI does not need the extra repository. Keep it as a development-time setting, or in a profile. If you put it in the pom, CI will use it too.

It keeps itself up to date

Once a library is under mvnpm control, a schedule checks the npm registry for new releases and syncs them. Dependabot then tells you there is a new version. When I recorded this, CodeMirror view 6.22 had been released four hours earlier and was already in mvnpm. WebJars was on 6.12. Just from us using it, there have been close to 650 releases.

Other bits

  • The mvnpm website lets you search packages, copy the dependency for your project, see the import map and walk the dependency tree. Dev UI uses import maps to resolve these dependencies.
  • Composites let you bundle a group of libraries into one artifact.
  • Lock files are not supported yet. Front-end libraries depend on ranges a lot, so I have started work on generating a lock file. For now you can pin versions as dependencies in your pom, which is verbose but works.

Update: lock files are now covered. Maven projects can use the mvnpm locker plugin, and Gradle projects can use Gradle's own dependency locking. The Getting Started guide has the details.


← All posts