|
| 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