-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add GraalJS QRCode demo using Maven plugin to install and bundle npm …
…dependencies.
- Loading branch information
Showing
13 changed files
with
12,231 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/". | ||
█▀▀▀▀▀█ ▀▄ ▀▄█▄▀ █▀▀▀▀▀█ | ||
█ ███ █ █▄ ▄ ▄▄▀▀ █ ███ █ | ||
█ ▀▀▀ █ █ ▄▀▀▄▄█ █ ▀▀▀ █ | ||
▀▀▀▀▀▀▀ █ █▄▀ █▄▀ ▀▀▀▀▀▀▀ | ||
█ ▀▀▀█▀▄ ▄█▀ █ ▀▄▄▀█▀▀▀▄ | ||
██▄ ▀▀▄ ▀▄▄█▀▀█▀█▀█▀▀ ▀█ | ||
██▀▀█▄▀█▄▄ ▄█▀▀▄█▀█▀▄▀█▀ | ||
█ ▄█▄▀▀ ▀▀ ▄▀█▀ █▀██▀ ▀█ | ||
▀ ▀ ▀▀ ██▄ ▀▀█▀█▀▀▀█▄▀ | ||
█▀▀▀▀▀█ ▄ ▄█▀▀ █ ▀ █▄▀▀█ | ||
█ ███ █ ███▀█▀▀▀█▀█▀█▄█▄▄ | ||
█ ▀▀▀ █ ▀▄▄▄ ▀█▄▄▄ ▄▄█▀ █ | ||
▀▀▀▀▀▀▀ ▀ ▀▀▀▀ ▀▀▀▀▀▀ | ||
``` |
Oops, something went wrong.