Skip to content

Commit 629d930

Browse files
Add GraalJS QRCode demo using Gradle and webpack. (#21)
Co-authored-by: Fabio Niephaus <[email protected]>
1 parent 8b1739b commit 629d930

File tree

22 files changed

+12479
-8
lines changed

22 files changed

+12479
-8
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Test GraalJS Gradle Webpack Guide
2+
3+
on:
4+
push:
5+
paths:
6+
- 'graaljs/graaljs-gradle-webpack-guide/**'
7+
- '.github/workflows/graaljs-gradle-webpack-guide.yml'
8+
pull_request:
9+
paths:
10+
- 'graaljs/graaljs-gradle-webpack-guide/**'
11+
- '.github/workflows/graaljs-gradle-webpack-guide.yml'
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
jobs:
17+
run:
18+
name: 'graaljs-gradle-webpack-guide'
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 15
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: graalvm/setup-graalvm@v1
24+
with:
25+
java-version: '24.0.1'
26+
distribution: 'graalvm'
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
cache: 'gradle'
29+
- name: Build and run 'graaljs-gradle-webpack-guide'
30+
run: |
31+
cd graaljs/graaljs-gradle-webpack-guide
32+
./gradlew build
33+
./gradlew run --args="https://www.graalvm.org/javascript"

graaljs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ This directory contains demo applications and guides for [GraalJS](https://www.g
1010

1111
## Guides
1212

13-
- [Use Node Packages in a Java SE application](graaljs-maven-webpack-guide/)
13+
- [Use Node Packages in a Java SE application with Maven](graaljs-maven-webpack-guide/)
14+
- [Use Node Packages in a Java SE application with Gradle](graaljs-gradle-webpack-guide/)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
.gradle
2+
build/
3+
!gradle/wrapper/gradle-wrapper.jar
4+
!**/src/main/**/build/
5+
!**/src/test/**/build/
6+
7+
### IntelliJ IDEA ###
8+
.idea/modules.xml
9+
.idea/jarRepositories.xml
10+
.idea/compiler.xml
11+
.idea/libraries/
12+
*.iws
13+
*.iml
14+
*.ipr
15+
out/
16+
!**/src/main/**/out/
17+
!**/src/test/**/out/
18+
19+
### Eclipse ###
20+
.apt_generated
21+
.classpath
22+
.factorypath
23+
.project
24+
.settings
25+
.springBeans
26+
.sts4-cache
27+
bin/
28+
!**/src/main/**/bin/
29+
!**/src/test/**/bin/
30+
31+
### NetBeans ###
32+
/nbproject/private/
33+
/nbbuild/
34+
/dist/
35+
/nbdist/
36+
/.nb-gradle/
37+
38+
### VS Code ###
39+
.vscode/
40+
41+
### Mac OS ###
42+
.DS_Store
43+
44+
node_modules/
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
2+
# Using Node Packages in a Java Application with Gradle
3+
4+
> **Note**: If you are using Maven, take a look at [this guide](../graaljs-maven-webpack-guide/).
5+
6+
JavaScript libraries can be packaged with plain Java applications.
7+
The integration is facilitated through [GraalJS](https://www.graalvm.org/javascript) and the [GraalVM Polyglot API](https://www.graalvm.org/latest/reference-manual/embed-languages/), supporting a wide range of project setups.
8+
9+
Using Node (NPM) packages in Java projects often requires a bit more setup, due to the nature of the Node packaging ecosystem.
10+
One way to use such modules is to prepackage them into a single _.js_ or _.mjs_ file using a bundler like [webpack](https://webpack.js.org/).
11+
This guide explains step-by-step how to integrate the webpack build into a Gradle Java project and embed the generated JavaScript code in the JAR file of the application.
12+
13+
# GraalJS QRCode Demo
14+
15+
## 1. Getting Started
16+
17+
In this guide, you will add the [qrcode](https://www.npmjs.com/package/qrcode) NPM package to a Java application to generate QR codes.
18+
19+
To complete this guide, you need the following:
20+
21+
* Some time on your hands
22+
* A decent text editor or IDE
23+
* JDK 21 or later
24+
* Gradle 8.0 or later
25+
26+
We recommend that you follow the instructions in the next sections and create the application step by step.
27+
However, you can go right to the completed example.
28+
29+
## 2. Setting Up the Gradle Project
30+
31+
You can start with any Gradle Java project. If you don’t have one yet:
32+
33+
```shell
34+
gradle init --type java-application
35+
```
36+
### 2.1. Adding the GraalJS Dependencies
37+
38+
Add the required dependencies for GraalJS in the `dependencies` block of your `build.gradle` file. This mirrors the dependency configuration in the [Maven guide](https://github.com/graalvm/graal-languages-demos/blob/main/graaljs/graaljs-maven-webpack-guide/pom.xml).
39+
40+
`build.gradle`
41+
```gradle
42+
dependencies {
43+
implementation 'org.graalvm.polyglot:polyglot:24.2.1' // ①
44+
implementation 'org.graalvm.polyglot:js:24.2.1' // ②
45+
}
46+
````
47+
48+
❶ The `polyglot` dependency provides the APIs to manage and use GraalJS from Java.
49+
50+
❷ The `js` dependency is a meta-package that transitively depends on all libraries and resources to run GraalJS.
51+
52+
### 2.2. Adding the Gradle Node Plugin
53+
54+
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 such as `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 [`com.github.node-gradle.node`](https://github.com/node-gradle/gradle-node-plugin) plugin to manage the download, installation, and bundling for you. This is the Gradle equivalent of the [`frontend-maven-plugin`](https://github.com/eirslett/frontend-maven-plugin) used in the [Maven guide](https://github.com/graalvm/graal-languages-demos/blob/main/graaljs/graaljs-maven-webpack-guide/pom.xml).
55+
56+
`build.gradle`
57+
58+
```gradle
59+
plugins {
60+
id 'java'
61+
id 'application'
62+
id 'com.github.node-gradle.node' version '7.0.1' // ①
63+
}
64+
65+
node { // ②
66+
version = '22.14.0'
67+
npmVersion = '10.9.2'
68+
download = true
69+
workDir = file("<span class="math-inline">\{project\.buildDir\}/node"\)
70+
npmWorkDir \= file\("</span>{project.buildDir}/npm")
71+
nodeProjectDir = file('src/main/js')
72+
}
73+
74+
tasks.register('webpackBuild', NpmTask) { // ③
75+
dependsOn tasks.npmInstall
76+
workingDir = file('src/main/js')
77+
args = ['run', 'build']
78+
environment = ['BUILD_DIR': "${buildDir}/classes/java/main/bundle"]
79+
}
80+
81+
processResources.dependsOn tasks.webpackBuild // ④
82+
83+
```
84+
85+
❶ Applies the node-gradle plugin, enabling Node.js and npm integration.
86+
87+
❷ Configures Node.js and npm versions, download settings, and working directories.
88+
89+
❸ Registers a 'webpackBuild' task to run 'npm run build' in the frontend directory. It ensures dependencies are installed first and sets the output directory.
90+
91+
❹ Ensures that the webpackBuild task is executed before the processResources task, so the bundled JavaScript is included in your JAR file.
92+
93+
## 3. Setting Up the JavaScript Build.
94+
95+
```shell
96+
mkdir src/main/js
97+
cd src/main/js
98+
```
99+
100+
Manual steps to set up the build environment:
101+
1. Run `npm init` and follow the instructions (package name: "qrdemo", entry point: "main.mjs").
102+
2. Run `npm install -D @webpack-cli/generators`.
103+
3. Run `npx webpack-cli init` and follow the instructions to set up a webpack project (select "ES6" and "npm").
104+
4. Run `npm install --save qrcode` to install and add the `qrcode` dependency.
105+
5. Run `npm install --save assert util stream-browserify browserify-zlib fast-text-encoding` to install the polyfill packages need to build with the webpack configuration below.
106+
107+
Alternatively, create a _package.json_ file with the following contents:
108+
```js
109+
{
110+
"name": "qrdemo",
111+
"version": "1.0.0",
112+
"description": "QRCode demo app",
113+
"main": "main.mjs",
114+
"scripts": {
115+
"test": "echo \"Error: no test specified\" && exit 1",
116+
"build": "webpack --mode=production --node-env=production",
117+
"build:dev": "webpack --mode=development",
118+
"build:prod": "webpack --mode=production --node-env=production",
119+
"watch": "webpack --watch"
120+
},
121+
"author": "",
122+
"license": "ISC",
123+
"dependencies": {
124+
"assert": "^2.1.0",
125+
"browserify-zlib": "^0.2.0",
126+
"fast-text-encoding": "^1.0.6",
127+
"qrcode": "^1.5.4",
128+
"stream-browserify": "^3.0.0",
129+
"util": "^0.12.5"
130+
},
131+
"devDependencies": {
132+
"@babel/core": "^7.25.2",
133+
"@babel/preset-env": "^7.25.4",
134+
"@webpack-cli/generators": "^3.0.7",
135+
"babel-loader": "^9.1.3",
136+
"webpack": "^5.94.0",
137+
"webpack-cli": "^5.1.4"
138+
}
139+
}
140+
```
141+
142+
Create a _webpack.config.js_ file, or open the one created by `webpack-cli init`, and fill it with the following contents:
143+
144+
```js
145+
const path = require('path');
146+
const { EnvironmentPlugin } = require('webpack');
147+
148+
const config = {
149+
entry: './main.mjs',
150+
output: {
151+
path: path.resolve(process.env.BUILD_DIR),
152+
filename: 'bundle.mjs',
153+
module: true,
154+
library: {
155+
type: 'module',
156+
},
157+
globalObject: 'globalThis'
158+
},
159+
experiments: {
160+
outputModule: true // Generate ES module sources
161+
},
162+
optimization: {
163+
usedExports: true, // Include only used exports in the bundle
164+
minimize: false, // Disable minification
165+
},
166+
resolve: {
167+
aliasFields: [], // Disable browser alias to use the server version of the qrcode package
168+
fallback: { // Redirect Node.js core modules to polyfills
169+
"stream": require.resolve("stream-browserify"),
170+
"zlib": require.resolve("browserify-zlib"),
171+
"fs": false // Exclude the fs module altogether
172+
},
173+
},
174+
plugins: [
175+
new EnvironmentPlugin({
176+
NODE_DEBUG: false, // Set process.env.NODE_DEBUG to false
177+
}),
178+
],
179+
};
180+
181+
module.exports = () => config;
182+
```
183+
Create `main.mjs`, the entry point of the bundle, with the following contents:
184+
```js
185+
// Re-export the "qrcode" module as a "QRCode" object in the exports of the bundle.
186+
export * as QRCode from 'qrcode';
187+
```
188+
189+
## 4. Using the JavaScript Library from Java
190+
191+
After reading the [qrcode](https://www.npmjs.com/package/qrcode) docs, you can write Java interfaces that match the [JavaScript types](https://www.npmjs.com/package/@types/qrcode) you want to use and methods you want to call on them.
192+
GraalJS makes it easy to access JavaScript objects via these interfaces.
193+
Java method names are mapped directly to JavaScript function and method names.
194+
The names of the interfaces can be chosen freely, but it makes sense to base them on the JavaScript types.
195+
196+
_src/main/java/com/example/QRCode.java_
197+
```java
198+
package com.example;
199+
200+
interface QRCode {
201+
Promise toString(String data);
202+
}
203+
```
204+
205+
_src/main/java/com/example/Promise.java_
206+
```java
207+
package com.example;
208+
209+
public interface Promise {
210+
Promise then(ValueConsumer onResolve);
211+
212+
Promise then(ValueConsumer onResolve, ValueConsumer onReject);
213+
}
214+
```
215+
216+
_src/main/java/com/example/ValueConsumer.java_
217+
```java
218+
package com.example;
219+
220+
import java.util.function.*;
221+
import org.graalvm.polyglot.*;
222+
223+
@FunctionalInterface
224+
public interface ValueConsumer extends Consumer<Value> {
225+
@Override
226+
void accept(Value value);
227+
}
228+
```
229+
230+
Using the `Context` class and these interfaces, you can now create QR codes and convert them to a Unicode string representation or an image.
231+
Our example just prints the QR code to `stdout`.
232+
233+
_src/main/java/com/example/App.java_
234+
```java
235+
package com.example;
236+
237+
import org.graalvm.polyglot.*;
238+
239+
public class App {
240+
public static void main(String[] args) throws Exception {
241+
try (Context context = Context.newBuilder("js")
242+
.allowHostAccess(HostAccess.ALL)
243+
.option("engine.WarnInterpreterOnly", "false")
244+
.option("js.esm-eval-returns-exports", "true")
245+
.option("js.unhandled-rejections", "throw")
246+
.option("js.text-encoding", "true")
247+
.build()) {
248+
Source bundleSrc = Source.newBuilder("js", App.class.getResource("/bundle/bundle.mjs")).build(); //
249+
Value exports = context.eval(bundleSrc);
250+
QRCode qrCode = exports.getMember("QRCode").as(QRCode.class); //
251+
String input = args.length > 0 ? args[0] : "https://www.graalvm.org/javascript/";
252+
Promise resultPromise = qrCode.toString(input); //
253+
resultPromise.then( //
254+
(Value output) -> {
255+
System.out.println("Successfully generated QR code for \"" + input + "\".");
256+
System.out.println(output.asString());
257+
}
258+
);
259+
}
260+
}
261+
}
262+
```
263+
264+
❶ Load the bundle generated by `webpack` from a resource embedded in the JAR file.
265+
266+
❷ JavaScript objects are returned using a generic [Value](https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/Value.html) type.
267+
You can cast the exported `QRCode` object to the declared `QRCode` interface so that you can use Java typing and IDE completion features.
268+
269+
`QRCode.toString` does not return the result directly but as a `Promise<string>` (alternatively, it can also be used with a callback).
270+
271+
❹ Invoke the `then` method of the `Promise` to eventually obtain the QRCode string and print it to `stdout`.
272+
273+
## 5. Running the Application
274+
275+
If you followed along with the example, you can now compile and run your application from the command line:
276+
277+
```shell
278+
./gradlew build
279+
./gradlew run --args="https://www.graalvm.org/"
280+
```
281+
The expected output should be similar to this:
282+
283+
```
284+
Successfully generated QR code for "https://www.graalvm.org/".
285+
286+
287+
    █▀▀▀▀▀█  ▀▄ ▀▄█▄▀ █▀▀▀▀▀█
288+
    █ ███ █ █▄ ▄ ▄▄▀▀ █ ███ █
289+
    █ ▀▀▀ █ █  ▄▀▀▄▄█ █ ▀▀▀ █
290+
    ▀▀▀▀▀▀▀ █ █▄▀ █▄▀ ▀▀▀▀▀▀▀
291+
    █ ▀▀▀█▀▄ ▄█▀ █ ▀▄▄▀█▀▀▀▄
292+
    ██▄  ▀▀▄ ▀▄▄█▀▀█▀█▀█▀▀ ▀█
293+
    ██▀▀█▄▀█▄▄  ▄█▀▀▄█▀█▀▄▀█▀
294+
    █ ▄█▄▀▀  ▀▀ ▄▀█▀ █▀██▀ ▀█
295+
    ▀  ▀ ▀▀ ██▄ ▀▀█▀█▀▀▀█▄▀
296+
    █▀▀▀▀▀█ ▄ ▄█▀▀  █ ▀ █▄▀▀█
297+
    █ ███ █ ███▀█▀▀▀█▀█▀█▄█▄▄
298+
    █ ▀▀▀ █ ▀▄▄▄ ▀█▄▄▄ ▄▄█▀ █
299+
    ▀▀▀▀▀▀▀ ▀ ▀▀▀▀     ▀▀▀▀▀▀
300+
```
301+
302+
## 6. Conclusion
303+
304+
By following this guide, you've learned how to:
305+
* Use GraalJS and the GraalVM Polyglot API to embed a JavaScript library in your Java application.
306+
* Use Webpack to bundle an NPM package into a self-contained _.mjs_ file, including its dependencies and polyfills for Node.js core modules that may be required to run on GraalJS.
307+
* Use the Gradle Node plugin to seamlessly integrate the `npm install` and `webpack` build steps into your Gradle project.
308+
309+
Feel free to use this demo as inspiration and a starting point for your own applications!
310+
311+

0 commit comments

Comments
 (0)