Skip to content

Commit

Permalink
Add GraalJS QRCode demo using Maven plugin to install and bundle npm …
Browse files Browse the repository at this point in the history
…dependencies.
  • Loading branch information
woess committed Sep 25, 2024
1 parent 901978d commit 013cb35
Show file tree
Hide file tree
Showing 13 changed files with 12,231 additions and 0 deletions.
330 changes: 330 additions & 0 deletions graaljs/graaljs-maven-webpack-qrcode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
# Using Node Packages in a Java Application

JavaScript libraries can be used in, and shipped with, plain Java applications.
The [GraalJS Maven artifacts](https://central.sonatype.com/artifact/org.graalvm.polyglot/js) and [GraalVM Polyglot APIs](https://www.graalvm.org/latest/reference-manual/embed-languages/) allow flexible integration with different project setups.

Using Node (NPM) packages in Java projects often requires a bit more setup, due to the nature of the Node packaging ecosystem.
One way to use such modules is pre-pack them into a single _.js_ or _.mjs_ file using a bundler like _webpack_.
This guide explains step-by-step how to integrate the _webpack_ build into a Maven Java project and embed the generated JavaScript code in the resulting JAR file.

## 1. Getting Started

In this guide, we will add a small NPM package to [generate QR codes](https://www.npmjs.com/package/qrcode) to a Java application:

To complete this guide, you will need the following:

* Some time on your hands
* A decent text editor or IDE
* JDK 21 or later
* Maven 3.6.3 or later

We recommend that you follow the instructions in the next sections and create the application step by step.
However, you can go right to the completed example.

## 2. Writing the application

You can start with any Maven application that runs on JDK 21 or later.
We will use a default Maven application [generated](https://maven.apache.org/archetypes/maven-archetype-quickstart/) from an archetype.

```shell
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.5 -DgroupId=com.example -DartifactId=qrdemo -DinteractiveMode=false
cd qrdemo
```

## 2.1 Dependency configuration

Add the required dependencies for GraalJS in the `<dependencies>` section of the POM.

`pom.xml`
```xml
<!-- <dependencies> -->
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId> <!---->
<version>24.1.0</version>
</dependency>

<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>js</artifactId> <!---->
<version>24.1.0</version>
<type>pom</type> <!---->
</dependency>
<!-- </dependencies> -->
```

❸ The `polyglot` dependency provides the APIs to manage and use GraalJS from Java.

❶ The `js` dependency is a meta-package that transitively depends on all libraries and resources to run GraalJS.

❷ Note that the `js` package is not a JAR - it is simply a `pom` that declares more dependencies.

## 2.2 Adding the Maven plugin

Most JavaScript packages are hosted on a package registry like [NPM](https://www.npmjs.com/) or [JSR](https://jsr.io/) and can be installed using a package manager like `npm`.
The Node.js ecosystem has conventions about the filesystem layout of installed packages that need to be kept in mind when embedding into Java.
To simplify the integration, a bundler can be used to repackage all dependencies in a single file.
You can use the [`frontend-maven-plugin`](https://github.com/eirslett/frontend-maven-plugin) to manage the download, installation, and bundling for you.

`pom.xml`
```xml
<!-- <build> -->
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>

<configuration>
<nodeVersion>v21.7.2</nodeVersion>
<workingDirectory>src/main/js</workingDirectory>
<installDirectory>target</installDirectory>
</configuration>

<executions>
<execution>
<!---->
<id>install node and npm</id>
<goals><goal>install-node-and-npm</goal></goals>
</execution>

<execution>
<!---->
<id>npm install</id>
<goals><goal>npm</goal></goals>
</execution>

<execution>
<!---->
<id>webpack build</id>
<goals><goal>webpack</goal></goals>
<configuration>
<arguments>--mode production</arguments>
<environmentVariables>
<BUILD_DIR>${project.build.outputDirectory}/bundle</BUILD_DIR>
</environmentVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<!-- </build> -->
```

❶ Installs `node` and `npm`.

❷ Runs `npm install` to download and install all npm packages in _src/main/js_.

❸ Runs `webpack` to build a bundle of the JS sources in _target/classes/bundle_, which will be later included in the application's JAR file and can be loaded as a resource.

## 2.3 Set up JavaScript build.

```shell
mkdir src/main/js
cd src/main/js
```

1. Run `npm init` and follow the instructions (package name: "qrdemo", entry point: "main.mjs").
2. Run `npm install -D @webpack-cli/generators`.
3. Run `npx webpack-cli init` and follow the instructions to set up a webpack project (select "ES6" and "npm").
4. Run `npm install --save qrcode` to install and add the `qrcode` dependency.
5. Run `npm install --save assert util stream-browserify browserify-zlib fast-text-encoding` to install polyfill packages needed by our webpack config (see below).

Alternatively, create a `package.json` file with the following contents, and run `npx webpack-cli init`:
```js
{
"name": "qrdemo",
"version": "1.0.0",
"description": "QRCode demo app",
"main": "main.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode=production --node-env=production",
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production",
"watch": "webpack --watch"
},
"author": "",
"license": "ISC",
"dependencies": {
"assert": "^2.1.0",
"browserify-zlib": "^0.2.0",
"fast-text-encoding": "^1.0.6",
"qrcode": "^1.5.4",
"stream-browserify": "^3.0.0",
"util": "^0.12.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@webpack-cli/generators": "^3.0.7",
"babel-loader": "^9.1.3",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4"
}
}
```

Open the `webpack.config.js` file that has been created by `webpack-cli init`, and add or replace the following properties:

```js
const path = require('path');
const { EnvironmentPlugin } = require('webpack');

const config = {
entry: './main.mjs',
output: {
path: path.resolve(process.env.BUILD_DIR),
filename: 'bundle.mjs',
module: true,
library: {
type: 'module',
},
globalObject: 'globalThis'
},
experiments: {
outputModule: true // Generate ES module sources
},
optimization: {
usedExports: true // Include only used exports in the bundle
minimize: false, // Disable minification
},
resolve: {
aliasFields: [], // Disable browser alias to use the server version of the qrcode package
fallback: { // Redirect Node.js core modules to polyfills
"stream": require.resolve("stream-browserify"),
"zlib": require.resolve("browserify-zlib"),
"fs": false // Exclude this module altogether
},
},
plugins: [
new EnvironmentPlugin({
NODE_DEBUG: false, // Set process.env.NODE_DEBUG to false
}),
],
};
module.exports = () => config;
```

Create `main.mjs`, the entry point of the bundle, with the following contents:
```js
// GraalJS doesn't have built-in TextEncoder support yet. It's easy to import it from a polyfill in the meantime.
import 'fast-text-encoding';

// Re-export the "qrcode" module as a "QRCode" object in the exports of the bundle.
export * as QRCode from 'qrcode';
```

## 2.4 Using the JavaScript library from Java

After reading the [qrcode](https://www.npmjs.com/package/qrcode) docs, we can write Java interfaces that match the [JavaScript types](https://www.npmjs.com/package/@types/qrcode) we want to use and methods we want to call on them.
GraalJS makes it easy to access JavaScript objects via these interfaces.
Java method names are mapped directly to JavaScript function and method names.
The names of the interfaces can be chosen freely, but it makes sense to base them on the JavaScript types.

`src/main/java/com/example/QRCode.java`
```java
package com.example;

interface QRCode {
Promise toString(String data);
}
```

`src/main/java/com/example/Promise.java`
```java
package com.example;

public interface Promise {
Promise then(ValueConsumer onResolve);

Promise then(ValueConsumer onResolve, ValueConsumer onReject);
}
```

`src/main/java/com/example/ValueConsumer.java`
```java
package com.example;

import java.util.function.*;
import org.graalvm.polyglot.*;

@FunctionalInterface
public interface ValueConsumer extends Consumer<Value> {
@Override
void accept(Value value);
}
```

Using the `Context` class and these interfaces, we can now create QR codes and convert them to a Unicode string representation or an image.
Our example just prints the QR code to stdout.

`src/main/java/com/example/App.java`
```java
package com.example;

import java.io.*;
import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;

public class App {
public static void main(String[] args) throws Exception {
try (Context context = Context.newBuilder("js")
.allowHostAccess(HostAccess.ALL)
.option("engine.WarnInterpreterOnly", "false")
.option("js.esm-eval-returns-exports", "true")
.option("js.unhandled-rejections", "throw")
.build()) {
Source bundleSrc = Source.newBuilder("js", App.class.getResource("/bundle/bundle.mjs")).build(); //
Value exports = context.eval(bundleSrc);
QRCode qrCode = exports.getMember("QRCode").as(QRCode.class); //
String input = argv.length > 0 ? argv[0] : "https://www.graalvm.org/javascript/";
Promise resultPromise = qrCode.toString(input); //
resultPromise.then( //
(Value output) -> {
System.out.println("Successfully generated QR code for \"" + input + "\".");
System.out.println(output.asString());
}
);
}
}
}
```

❶ We load the bundle generated by _webpack_ from a resource embedded in the JAR file.

❷ JavaScript objects are returned using a generic [Value](https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/Value.html) type.
We cast the exported `QRCode` object to our declared `QRCode` interface so we can use Java typing and IDE completion features.

`QRCode.toString` does not return the result directly but as a `Promise<string>` (alternatively, it can also be used with a callback).

❹ We invoke the `then` method of the `Promise` to eventually obtain the QRCode string and print it to stdout.

## 3. Running the application

If you followed along with the example, you can now compile and run your application from the commandline:

```shell
mvn package
mvn exec:java -Dexec.mainClass=org.example.App -Dexec.args="https://www.graalvm.org/"
```

Expected output:
```
Successfully generated QR code for "https://www.graalvm.org/".
█▀▀▀▀▀█ ▀▄ ▀▄█▄▀ █▀▀▀▀▀█
█ ███ █ █▄ ▄ ▄▄▀▀ █ ███ █
█ ▀▀▀ █ █ ▄▀▀▄▄█ █ ▀▀▀ █
▀▀▀▀▀▀▀ █ █▄▀ █▄▀ ▀▀▀▀▀▀▀
█ ▀▀▀█▀▄ ▄█▀ █ ▀▄▄▀█▀▀▀▄
██▄ ▀▀▄ ▀▄▄█▀▀█▀█▀█▀▀ ▀█
██▀▀█▄▀█▄▄ ▄█▀▀▄█▀█▀▄▀█▀
█ ▄█▄▀▀ ▀▀ ▄▀█▀ █▀██▀ ▀█
▀ ▀ ▀▀ ██▄ ▀▀█▀█▀▀▀█▄▀
█▀▀▀▀▀█ ▄ ▄█▀▀ █ ▀ █▄▀▀█
█ ███ █ ███▀█▀▀▀█▀█▀█▄█▄▄
█ ▀▀▀ █ ▀▄▄▄ ▀█▄▄▄ ▄▄█▀ █
▀▀▀▀▀▀▀ ▀ ▀▀▀▀ ▀▀▀▀▀▀
```
Loading

0 comments on commit 013cb35

Please sign in to comment.