├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── docsite ├── docs │ ├── concurrency.md │ ├── index.md │ ├── language-embedding.png │ ├── language-injection.md │ ├── running-polyglot-programs.md │ ├── types.md │ ├── using-from-java.md │ └── using-from-kotlin.md └── mkdocs.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── spinners-sample ├── build.gradle.kts ├── package-lock.json ├── package.json └── src │ └── main │ ├── java │ └── net │ │ └── plan99 │ │ └── nodejs │ │ └── sample │ │ └── spinners │ │ └── java │ │ └── SpinnerJavaDemo.java │ └── kotlin │ └── net │ └── plan99 │ └── nodejs │ └── sample │ └── spinners │ └── SpinnerDemo.kt └── src └── main ├── java ├── Demo.java └── net │ └── plan99 │ └── nodejs │ ├── java │ └── NodeJS.java │ └── kotlin │ └── NodeJS.kt └── resources ├── boot.js └── nodejvm /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | node_modules/ 7 | .idea/ 8 | 9 | # User-specific stuff 10 | 11 | ### Gradle template 12 | .gradle 13 | build/ 14 | 15 | # Ignore Gradle GUI config 16 | gradle-app.setting 17 | 18 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 19 | !gradle-wrapper.jar 20 | 21 | # Cache of project 22 | .gradletasknamecache 23 | 24 | dat-sample/download 25 | dat-sample/dumps 26 | 27 | docsite/site 28 | docsite/docs/kotlin-api -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJVM 2 | 3 | This tool starts up GraalVM in a way that gives you access to a genuine NodeJS instance. [GraalVM](https://www.graalvm.org) is a variant of OpenJDK with a fast, modern JavaScript engine capable of running all NodeJS modules. GraalVM gives you a version of NodeJS that can access Java classes, but what if you want the other way around - Java code accessing NodeJS modules? 4 | 5 | NodeJVM is a thin wrapper script and JAR that sets up the JVM with full access to NodeJS. It provides a simple API for calling in and out of the Node event loop thread, in a thread-safe way. It also provides a Kotlin API that offers many conveniences. 6 | 7 | Note: *NodeJVM is not a new JVM*. It's just a way to start up GraalVM, which is a JVM produced by Oracle with a set of patches on top of OpenJDK. 8 | 9 | # Why use NPM modules from Java? 10 | 11 | * Gain access to unique JavaScript modules, like the DAT peer to peer file sharing framework shown in the sample. 12 | * Combine your existing NodeJS and Java servers together, eliminating the overheads of REST, serialisation, two separate 13 | virtual machines. Simplify your microservices architecture into being a polyglot architecture instead. 14 | * Use it to start porting NodeJS apps to the JVM world and languages, incrementally, one chunk at a time, whilst always 15 | having a runnable app. 16 | 17 | # Documentation 18 | 19 | 📚 [Access the documentation site](https://mikehearn.github.io/nodejvm/) 20 | 21 | # What does it look like? 22 | 23 | IntelliJ Ultimate edition users can get integrated "language injection", in which a single editor tab can use syntax highlighting/code completion/refactoring support from multiple languages simultaneously. Combined with Kotlin's multi-line string syntax, it looks like this: 24 | 25 | ![Screenshot of language injection](docsite/docs/language-embedding.png) 26 | 27 | # TODO 28 | 29 | - Gradle plugin? 30 | - Windows support when GraalVM has caught up. 31 | - Can node_modules directories be packaged as JARs? 32 | 33 | # License 34 | 35 | Apache 2.0 36 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | import org.jetbrains.dokka.gradle.DokkaTask 3 | import org.jetbrains.dokka.plugability.configuration 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 5 | import java.net.URI 6 | import java.net.URL 7 | 8 | plugins { 9 | java 10 | kotlin("jvm") version "1.9.25" 11 | `maven-publish` 12 | id("org.jetbrains.dokka") version "1.9.20" 13 | } 14 | 15 | group = "net.plan99.nodejs" 16 | version = "1.2" 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | compileOnly("org.jetbrains:annotations:16.0.2") 24 | compileOnly(kotlin("stdlib-jdk8")) 25 | api("org.graalvm.polyglot:polyglot:24.1.1") 26 | } 27 | 28 | kotlin { 29 | jvmToolchain(21) 30 | } 31 | 32 | java { 33 | toolchain { 34 | languageVersion.set(JavaLanguageVersion.of(21)) 35 | } 36 | } 37 | 38 | tasks.withType { 39 | // Not strictly needed but it's nice for Java users to always have parameter reflection info. 40 | options.compilerArgs.add("-parameters") 41 | } 42 | 43 | val genNodeJVMScript = task("genNodeJVMScript") { 44 | val bootjs = "src/main/resources/boot.js" 45 | inputs.file(bootjs) 46 | from("src/main/resources/nodejvm") 47 | into(layout.buildDirectory.dir("nodejvm")) 48 | filter(ReplaceTokens::class, mapOf( 49 | "tokens" to mapOf( 50 | "bootjs" to file(bootjs).readText(), 51 | "ver" to version.toString() 52 | ) 53 | )) 54 | filePermissions { 55 | unix(0x000001ed) // rwxr-xr-x permissions, kotlin doesn't support octal literals 56 | } 57 | } 58 | 59 | val copyInteropJar = task("copyInteropJar") { 60 | dependsOn(":jar") 61 | from(layout.buildDirectory.file("libs/nodejs-interop-$version.jar")) 62 | into(layout.buildDirectory.dir("nodejvm")) 63 | } 64 | 65 | tasks["build"].dependsOn(genNodeJVMScript, copyInteropJar) 66 | 67 | tasks.register("sourcesJar") { 68 | from(sourceSets.main.get().allJava) 69 | archiveClassifier.set("sources") 70 | } 71 | 72 | tasks.register("javadocJar") { 73 | from(tasks.javadoc) 74 | archiveClassifier.set("javadoc") 75 | } 76 | 77 | publishing { 78 | publications { 79 | create("api") { 80 | from(components["java"]) 81 | groupId = "net.plan99" 82 | artifactId = "nodejvm" 83 | 84 | artifact(tasks["sourcesJar"]) 85 | artifact(tasks["javadocJar"]) 86 | 87 | pom { 88 | name.set("NodeJVM") 89 | description.set("Easier NodeJS interop for GraalVM") 90 | licenses { 91 | license { 92 | name.set("The Apache License, Version 2.0") 93 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 94 | } 95 | } 96 | developers { 97 | developer { 98 | id.set("mike") 99 | name.set("Mike Hearn") 100 | email.set("mike@plan99.net") 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | repositories { 108 | maven { 109 | // change to point to your repo, e.g. http://my.org/repo 110 | url = uri(layout.buildDirectory.dir("repo")) 111 | } 112 | } 113 | } 114 | 115 | tasks.dokkaHtml { 116 | outputDirectory.set(layout.projectDirectory.dir("docsite/docs/kotlin-api")) 117 | dokkaSourceSets.configureEach { 118 | externalDocumentationLink { 119 | url = URI("https://www.graalvm.org/sdk/javadoc/").toURL() 120 | packageListUrl = URI("https://www.graalvm.org/sdk/javadoc/package-list").toURL() 121 | } 122 | reportUndocumented = true 123 | 124 | // Exclude the Java API. 125 | perPackageOption { 126 | matchingRegex.set("net\\.plan99\\.nodejs\\.java.*") 127 | suppress = true 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /docsite/docs/concurrency.md: -------------------------------------------------------------------------------- 1 | # Concurrency and access to the JavaScript world 2 | 3 | Java and JavaScript execute on different threads by default, and thus will execute concurrently. 4 | 5 | !!! warning 6 | 7 | **You can only access JavaScript objects from the NodeJS thread.** 8 | 9 | This is important. NodeJS will use the JVM heap, so you can store *references* to JS objects wherever you like, 10 | however, due to the need to synchronize with the Node event loop, even something as simple as calling 11 | `toString()` on a JavaScript object will fail unless you are on the right thread. 12 | 13 | To run NodeJS code you must therefore *enter the Node thread*. In Java this is done by passing a lambda into 14 | `NodeJS.runJS` or `NodeJS.runJSAsync`. The calling Java thread will pause, wait for NodeJS to reach its main 15 | loop (if it's doing something) and then the lambda will be executed on the node thread. You can then call in 16 | and out of JavaScript to your hearts content: 17 | 18 | ```java 19 | // Will block and wait for the JavaScript thread to become available. 20 | int result = NodeJS.runJS(() -> 21 | NodeJS.eval("return 2 + 3 + 4").asInt() 22 | ); 23 | ``` 24 | 25 | In Kotlin, entering a `nodejs { }` block will synchronize with the NodeJS thread, and you can pass return 26 | values out of this block. 27 | 28 | It's safe to enter the NodeJS thread anywhere. You can nest entries inside each other, as if you enter 29 | Node whilst already on the event loop thread it will simply execute the lambda/code block immediately. 30 | 31 | Just remember not to block the NodeJS main thread itself: everything in JavaScript land is event driven. 32 | Things you might do that accidentally halt all JavaScript execution include: 33 | 34 | * Reading or writing to a socket. 35 | * Accessing a file (may be slow if it's over a network mount). 36 | * Call Thread.sleep(). 37 | * Do a long and intensive calculation. 38 | 39 | ## Futures 40 | 41 | A JavaScript Promise can be converted to a `CompletableFuture` by using `NodeJS.futureFromPromise` (in Java) or by 42 | calling the `Value.toCompletableFuture()` extension method inside a `nodejs` block in Kotlin. When the promise is 43 | resolved the future is completed. 44 | -------------------------------------------------------------------------------- /docsite/docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This repository demonstrates how to use NodeJS/npm modules directly from Java and Kotlin. Why is it useful: 4 | 5 | * Gain access to unique JavaScript modules, like the Dat peer to peer file sharing framework shown in the samples. 6 | * Combine your existing NodeJS and Java servers together, eliminating the overheads of REST, serialisation, two separate 7 | virtual machines. Simplify your microservices architecture into being a polyglot architecture instead. 8 | * Use it to start porting NodeJS apps to the JVM world and languages, incrementally, one chunk at a time, whilst always 9 | having a runnable app. Or do the reverse. 10 | 11 | ## How does it work? 12 | 13 | [GraalVM](https://www.graalvm.org/) is a modified version of OpenJDK that includes the cutting edge Graal and Truffle compiler infrastructure. 14 | It provides an advanced JavaScript engine that has competitive performance with V8, and also a modified version of 15 | NodeJS 10 that swaps out V8 for this enhanced JVM. In this way you can fuse together NodeJS and the JVM, allowing apps 16 | to smoothly access both worlds simultaneously with full JIT compilation. 17 | 18 | ## Known limitations 19 | 20 | NodeJS really wants to load module files from the filesystem and nowhere else, so your Java app will need a `node_modules` 21 | directory from where it's started. There are tricks to work around this and allow bundling of JS into JAR files as 22 | libraries, but nothing done at the moment. 23 | 24 | GraalVM uses NodeJS 10, not the latest versions. 25 | 26 | You change `java` on the command line to `nodejvm` and that's all it needs, but many tools and IDEs expect the java 27 | launcher to always be called `java`. -------------------------------------------------------------------------------- /docsite/docs/language-embedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/nodejvm/6cddffd0332bfc3b6b8ddbc2ae598f5870a104e2/docsite/docs/language-embedding.png -------------------------------------------------------------------------------- /docsite/docs/language-injection.md: -------------------------------------------------------------------------------- 1 | # Language injection 2 | 3 | IntelliJ offers "language injection", which means a file can contain multiple languages at once. 4 | To benefit from this you unfortunately have to flip a switch in the IDE settings: 5 | 6 | 1. Open your preferences and go to `Editor > Language Injection > Advanced` 7 | 2. Under "Performance" select "Enable data flow analysis" 8 | 9 | Any string passed to `eval` will now be highlighted and edited as JavaScript, not a 10 | Java/Kotlin/Scala/etc string literal. Like this: 11 | 12 | ![Screenshot of language injection](language-embedding.png) 13 | 14 | !!! question 15 | 16 | If the feature doesn't seem to work, make sure you aren't passing the string through some other 17 | function first. Kotlin multi-line strings often get a `.trimIndent()` appended automatically, 18 | which is unfortunately sufficient to break the dataflow analysis and stop IntelliJ recognising 19 | that the language of the string hasn't changed. You can just take it out: JS isn't sensitive 20 | to leading whitespace. -------------------------------------------------------------------------------- /docsite/docs/running-polyglot-programs.md: -------------------------------------------------------------------------------- 1 | # Running polyglot programs 2 | 3 | Download the latest release from the [releases page](https://github.com/mikehearn/nodejvm/releases). 4 | 5 | Now add the `nodejvm` directory to your path, or copy the contents to somewhere on your path. 6 | 7 | Make sure you've got GraalVM and set the location as either `JAVA_HOME` or `GRAALVM_PATH`. Or make sure 8 | its `bin` directory is on your path. 9 | 10 | Start your Java programs as normal but run `nodejvm` instead of `java`, e.g. 11 | 12 | `nodejvm -cp "libs/*.jar" my.main.Class arg1 arg2` 13 | 14 | ## Running the samples 15 | 16 | Check out the NodeJVM repository. Then try: 17 | 18 | ``` 19 | gradle dat-sample:run 20 | ``` 21 | 22 | It should join the DAT network and might print some peer infos, depending on your luck. 23 | 24 | Also try something a bit less Gradley: 25 | 26 | ``` 27 | gradle build spinners-sample:shadowJar 28 | ../build/nodejvm/nodejvm -jar build/libs/spinners-sample-*-all.jar 29 | ``` 30 | 31 | ## From your own Gradle projects 32 | 33 | Firstly, add my Maven repository for the interop API JAR (this step will become obsolete soon as it'll be in JCenter): 34 | 35 | ``` 36 | import java.net.URI 37 | 38 | repositories { 39 | maven { 40 | url = URI("https://dl.bintray.com/mikehearn/open-source") 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation("net.plan99:nodejvm:1.1") 46 | } 47 | ``` 48 | 49 | (these all use Kotlin DSL syntax) 50 | 51 | Then adjust your JavaCompile tasks to run `nodejvm` instead of `java`: 52 | 53 | ``` 54 | tasks.withType { 55 | executable("nodejvm") 56 | } 57 | ``` 58 | 59 | This requires `nodejvm` to be on your PATH and JAVA_HOME to be pointed at GraalVM. 60 | There's no support for automatically downloading NodeJVM or Graal itself at this 61 | time. 62 | 63 | ## Packaging 64 | 65 | If you use the `application` plugin to generate startup scripts, then you will need a bit of extra code to edit the 66 | script as it really wants the Java runner to be called `java` and not anything else. Try adding this code to your 67 | build script (you may need to run `gradle installDist --rerun-tasks` after): 68 | 69 | ```kotlin 70 | tasks.startScripts { 71 | val script = outputDir!!.resolve("doppelganger") 72 | doLast { 73 | script.writeText(script.readText().replace("JAVACMD=java", "JAVACMD=nodejvm")) 74 | } 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /docsite/docs/types.md: -------------------------------------------------------------------------------- 1 | # Type conversions 2 | 3 | The Graal API uses a variant type called `Value`. 4 | 5 | JavaScript objects returned through eval are mapped to Java/JVM world objects in a fairly sophisticated way, as described 6 | in the [Graal SDK documentation](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#as-java.lang.Class-). 7 | 8 | Briefly: 9 | 10 | * Strings and numbers work as you'd expect. JavaScript numbers can be larger than a Java number type, 11 | if the number wouldn't fit then `ClassCastException` is thrown. 12 | * Date and time values are mapped to `java.time` types. 13 | * Exceptions can be mapped to `PolyglotException`. 14 | * Things with members i.e. objects can be mapped to `Map` 15 | * JavaScript lists can be converted to `List` or a Java array. It's more efficient to use `List`. 16 | * JavaScript functions can be converted to lambdas/functional interfaces. 17 | * JavaScript objects can be converted to interfaces, which get special behaviours (see below). 18 | 19 | You can also go the other way. 20 | 21 | ## Interfaces 22 | 23 | You can cast a `Value` to an interface. Special rules apply that map JavaBean property name conventions 24 | to JavaScript. If a method starts with "get" or "is" then calling it is treated as a property read. 25 | If a method starts with "set" then calling it is treated as a property write, with the names being 26 | mapped appropriately. 27 | 28 | Kotlin `val` and `var` map to JavaBean style methods under the hood, so they should map transparently. 29 | 30 | ## Automatic conversion from TypeScript 31 | 32 | There is a project called Dukat that is working on automatic conversion of TypeScript to Kotlin 33 | declarations. Because Kotlin code can also be accessed from Java, this would be also useful for 34 | Java developers. Dukat is still under heavy development and would need some small adjustments for 35 | NodeJVM, but it's an avenue for future work. -------------------------------------------------------------------------------- /docsite/docs/using-from-java.md: -------------------------------------------------------------------------------- 1 | # Using from Java 2 | 3 | The `NodeJS` class gives you access to the JavaScript runtime: 4 | 5 | ```java 6 | import net.plan99.nodejs.NodeJS; 7 | 8 | public class Demo { 9 | public static void main(String[] args) { 10 | int result = NodeJS.runJS(() -> 11 | NodeJS.eval("return 2 + 3 + 4").asInt() 12 | ); 13 | System.out.println(result); 14 | } 15 | } 16 | ``` 17 | 18 | Evaluate JavaScript code with the `eval` static method. Before you can use it, you need to get yourself onto the 19 | NodeJS main thread by providing a lambda to `NodeJS.runJS`. See below for more info on this. 20 | 21 | What you get back from `eval` is a GraalVM Polyglot `Value` class ([javadoc](http://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html)). 22 | [Documentation for the Polyglot API is here](http://www.graalvm.org/sdk/javadoc/). 23 | 24 | There is also a `NodeJS.runJSAsync` method which returns a `CompletableFuture` with the result of the lambda, instead 25 | of waiting, and an `Executor` that executes jobs on the NodeJS thread. 26 | 27 | The executor can be useful if you don't want to return anything from the JS code: 28 | 29 | ```java 30 | NodeJS.executor.execute(() -> { 31 | NodeJS.eval("require('some-module').doSomething()"); 32 | } 33 | ``` 34 | 35 | But you can also use it in many other ways, as the `Executor` type is a general way to schedule work onto other threads 36 | in Java. 37 | 38 | ## Casting 39 | 40 | It can be useful to convert `Value` to a more convenient type. Many such [type conversions](/types) are available. 41 | 42 | The Polyglot API has conveniences for many of them on the `Value` object itself, like `Value.asBoolean()`, `Value.asString()`, 43 | `Value.asLong()` etc. 44 | 45 | For objects, NodeJVM provides an extra bit of glue to make JavaBean style properties work. To benefit, you must 46 | unfortunately use a little boilerplate to ensure generics are preserved. Cast like this: 47 | 48 | ```java 49 | MyInterface ora = NodeJS.castValue(v, new TypeLiteral() {}); 50 | ``` 51 | 52 | If you passed a Java object into JavaScript and have now got it back again, use `Value.asHostObject()` 53 | to unwrap it. 54 | 55 | ## Bindings 56 | 57 | You will often want to put a Java object into the JavaScript environment. That's done using the Polyglot 58 | bindings API. The bindings are a string to object map which acts as a kind of transfer area. You insert 59 | objects into the bindings with a name, and then in JavaScript use `Polyglot.import()` to retrieve it: 60 | 61 | ```java 62 | class Bindings { 63 | static void demo() { 64 | NodeJS.polyglotContext().getPolyglotBindings().putMember( 65 | "props", 66 | System.getProperties() 67 | ); 68 | NodeJS.executor.execute(() -> { 69 | NodeJS.eval( 70 | "const props = Polyglot.import('props');" + 71 | "console.log(props.get('java.version'));" 72 | ); 73 | }); 74 | } 75 | } 76 | ``` 77 | 78 | This program will expose the Java system properties to JavaScript and then print the Java runtime version. -------------------------------------------------------------------------------- /docsite/docs/using-from-kotlin.md: -------------------------------------------------------------------------------- 1 | # Using from Kotlin 2 | 3 | 📚 **[Read the Kotlin API Docs](kotlin-api/nodejs-interop/net.plan99.nodejs.kotlin/-node-j-s-a-p-i/index.html)** 4 | 5 | Kotlin provides many features that make it much more convenient and pleasant to work with NodeJS from the JVM. 6 | The API is available only inside a `nodejs { }` block. You may evaluate JavaScript when inside a `nodejs` block, like so: 7 | 8 | ```kotlin 9 | val i: Int = nodejs { 10 | eval("2 + 2 + 4") 11 | } 12 | ``` 13 | 14 | Kotlin's type inference combined with GraalJS and the Polyglot infrastructure ensures that you can take the result 15 | of `eval` and stick it into a normal Kotlin variable most of the time. Polyglot casts will be performed automatically. 16 | 17 | If you don't want any return value, use `run` instead of `eval`: 18 | 19 | ```kotlin 20 | nodejs { 21 | run("console.log('hi there, world')") 22 | } 23 | ``` 24 | 25 | The `nodejs` block synchronises with the NodeJS event loop, thus making access to the JavaScript engine safe. 26 | 27 | Remember Kotlin supports **multi-line strings** using `"""`. This is extremely convenient for embedding 28 | JavaScript into Kotlin files. If the string is passed in via a simple dataflow (with no intermediate methods) 29 | then IntelliJ will properly code highlight and do auto-complete for the embedded JavaScript via the 30 | [language injection](/language-injection) feature! Just watch out that a pointless `.trimIndent()` doesn't 31 | sneak in there, which will break injection. 32 | 33 | ## Values 34 | 35 | `Value` is GraalVM's generic variant type. It can be used to represent any JavaScript value. If you ask `eval` 36 | for a `Value`, you can use Kotlin's indexing operators to treat it as a dictionary: 37 | 38 | ```kotlin 39 | nodejs { 40 | val v: Value = eval("process.memoryUsage()") 41 | val heapTotal: Long = v["heapTotal"] 42 | println("JS heap total size is $heapTotal") 43 | } 44 | ``` 45 | 46 | When evaluated in NodeJS `process.memoryUsage()` will give you something like this: 47 | 48 | ``` 49 | > process.memoryUsage() 50 | { rss: 22847488, 51 | heapTotal: 9682944, 52 | heapUsed: 6075560, 53 | external: 12318 } 54 | ``` 55 | 56 | So you can see how to use Kotlin's property access syntax in the same way you might in JS itself. 57 | 58 | But usually it's easier and better to cast a `Value` to some other more native type. 59 | Read about [type conversions](/types) to learn how what's possible. To cast, you can either just ensure the return 60 | value of `eval` is assigned to the correct type, or you can use `.cast` on a `Value`, like 61 | this: 62 | 63 | ```kotlin 64 | val str: String = value.cast() 65 | val str2 = value.cast() 66 | ``` 67 | 68 | ## Top level variable binding 69 | 70 | You'll often want to pass Java/Kotlin objects into the JS world. You can do this by binding a JavaScript variable 71 | to a Kotlin variable and then reading/writing to it as normal: 72 | 73 | ```kotlin 74 | nodejs { 75 | var list: List by bind(listOf("a", "b", "c")) 76 | run("console.log(list[0])") 77 | 78 | run("x = 5") 79 | val x by bind() 80 | println("$x == 5") 81 | } 82 | ``` 83 | 84 | `bind` is a function that optionally takes a default value and then connects the new variable to a top level JS 85 | variable with the same name. 86 | 87 | Recall that in JavaScript `var` creates a locally scoped variable, so to interop like this you must define JS variables 88 | at the top level without `var`. That's why we run `x = 5` above and not `var x = 5`. If we had used `var` then Kotlin 89 | wouldn't be able to see it. 90 | 91 | ## Interfaces 92 | 93 | It's highly convenient to cast `Value` to interfaces. NodeJVM adds some extra proxying on top of GraalVM Polyglot to 94 | make Kotlin (i.e. JavaBean) style properties map to JavaScript properties correctly. Here's how you can use 95 | the `ora` module that provides fancy spinners using the support for interface casting: 96 | 97 | ```kotlin 98 | interface Ora { 99 | fun start(text: String) 100 | fun info(text: String) 101 | fun warn(text: String) 102 | fun error(text: String) 103 | 104 | var text: String 105 | var prefixText: String? 106 | var color: String 107 | } 108 | 109 | fun main() { 110 | val spinner: Ora = nodejs { eval("require('ora')()") } 111 | 112 | nodejs { 113 | spinner.start("Loading unicorns ...") 114 | } 115 | 116 | // We don't want to block the nodeJS thread by sleeping inside a nodejs{} block. 117 | // This sleep represents some sort of "work". 118 | Thread.sleep(2000) 119 | 120 | // Change color and message. 121 | nodejs { 122 | spinner.color = "red" 123 | spinner.text = "Loading rainbows" 124 | spinner.prefixText = "Working" 125 | } 126 | } 127 | ``` 128 | 129 | If a method in such an interface is defined to return `CompletableFuture` then the method is expected to return 130 | a promise, and the two will be linked. The type parameter cannot currently be casted automatically, as it's erased. 131 | 132 | ## Callbacks and lambdas 133 | 134 | You unfortunately cannot pass Kotlin lambdas straight into JavaScript due to [KT-30107](https://youtrack.jetbrains.com/issue/KT-301070). 135 | So you have to use the Java functional types instead, like this: 136 | 137 | ```kotlin 138 | nodejs { 139 | var callback1 by bind(Consumer { m: Map -> 140 | val obj = m.asValue().cast() 141 | println("rss is ${obj.rss()}") 142 | }) 143 | run("callback1(process.memoryUsage())") 144 | } 145 | ``` 146 | 147 | Due to a GraalJS bug, it always passes a JavaScript object into a lambda as Map, but you can easily 148 | convert it to an interface as seen above. [Alternatively just make it a real public class](https://github.com/graalvm/graaljs/issues/120). 149 | -------------------------------------------------------------------------------- /docsite/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: NodeJVM 2 | site_description: Smooth and easy NodeJS interop for the JVM 3 | site_author: Mike Hearn 4 | 5 | repo_name: mikehearn/nodejvm 6 | repo_url: https://github.com/mikehearn/nodejvm 7 | 8 | theme: 9 | name: 'material' 10 | palette: 11 | primary: 'blue' 12 | accent: 'blue' 13 | 14 | markdown_extensions: 15 | - admonition 16 | - codehilite 17 | - toc: 18 | permalink: true 19 | 20 | nav: 21 | - index.md 22 | - running-polyglot-programs.md 23 | - concurrency.md 24 | - types.md 25 | - using-from-java.md 26 | - using-from-kotlin.md 27 | - language-injection.md -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/nodejvm/6cddffd0332bfc3b6b8ddbc2ae598f5870a104e2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | maven { url 'https://plugins.gradle.org/m2/' } 5 | } 6 | } 7 | rootProject.name = 'nodejs-interop' 8 | include 'spinners-sample' 9 | -------------------------------------------------------------------------------- /spinners-sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | java 5 | kotlin("jvm") 6 | application 7 | id("com.github.johnrengelman.shadow") version("8.1.1") 8 | } 9 | 10 | group = "net.plan99.nodejs" 11 | version = "1.0" 12 | 13 | application { 14 | mainClass.set("net.plan99.nodejs.sample.spinners.SpinnerDemoKt") 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | kotlin { 21 | jvmToolchain(21) 22 | } 23 | dependencies { 24 | implementation(kotlin("stdlib-jdk8")) 25 | implementation(rootProject) // This would be: implementation("net.plan99.nodejs:nodejs-interop:1.0") in a real project 26 | } 27 | 28 | tasks.withType { 29 | dependsOn(":build") 30 | executable("${rootProject.buildDir}/nodejvm/nodejvm") 31 | } 32 | -------------------------------------------------------------------------------- /spinners-sample/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spinners-sample", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-regex": { 8 | "version": "4.1.0", 9 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 10 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" 11 | }, 12 | "ansi-styles": { 13 | "version": "3.2.1", 14 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 15 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 16 | "requires": { 17 | "color-convert": "^1.9.0" 18 | } 19 | }, 20 | "chalk": { 21 | "version": "2.4.2", 22 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 23 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 24 | "requires": { 25 | "ansi-styles": "^3.2.1", 26 | "escape-string-regexp": "^1.0.5", 27 | "supports-color": "^5.3.0" 28 | } 29 | }, 30 | "cli-cursor": { 31 | "version": "3.1.0", 32 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 33 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 34 | "requires": { 35 | "restore-cursor": "^3.1.0" 36 | } 37 | }, 38 | "cli-spinners": { 39 | "version": "2.2.0", 40 | "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz", 41 | "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==" 42 | }, 43 | "clone": { 44 | "version": "1.0.4", 45 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", 46 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" 47 | }, 48 | "color-convert": { 49 | "version": "1.9.3", 50 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 51 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 52 | "requires": { 53 | "color-name": "1.1.3" 54 | } 55 | }, 56 | "color-name": { 57 | "version": "1.1.3", 58 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 59 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 60 | }, 61 | "defaults": { 62 | "version": "1.0.3", 63 | "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", 64 | "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", 65 | "requires": { 66 | "clone": "^1.0.2" 67 | } 68 | }, 69 | "escape-string-regexp": { 70 | "version": "1.0.5", 71 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 72 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 73 | }, 74 | "has-flag": { 75 | "version": "3.0.0", 76 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 77 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 78 | }, 79 | "is-interactive": { 80 | "version": "1.0.0", 81 | "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", 82 | "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" 83 | }, 84 | "log-symbols": { 85 | "version": "3.0.0", 86 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", 87 | "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", 88 | "requires": { 89 | "chalk": "^2.4.2" 90 | } 91 | }, 92 | "mimic-fn": { 93 | "version": "2.1.0", 94 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 95 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" 96 | }, 97 | "onetime": { 98 | "version": "5.1.0", 99 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 100 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 101 | "requires": { 102 | "mimic-fn": "^2.1.0" 103 | } 104 | }, 105 | "ora": { 106 | "version": "4.0.2", 107 | "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.2.tgz", 108 | "integrity": "sha512-YUOZbamht5mfLxPmk4M35CD/5DuOkAacxlEUbStVXpBAt4fyhBf+vZHI/HRkI++QUp3sNoeA2Gw4C+hi4eGSig==", 109 | "requires": { 110 | "chalk": "^2.4.2", 111 | "cli-cursor": "^3.1.0", 112 | "cli-spinners": "^2.2.0", 113 | "is-interactive": "^1.0.0", 114 | "log-symbols": "^3.0.0", 115 | "strip-ansi": "^5.2.0", 116 | "wcwidth": "^1.0.1" 117 | } 118 | }, 119 | "restore-cursor": { 120 | "version": "3.1.0", 121 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 122 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 123 | "requires": { 124 | "onetime": "^5.1.0", 125 | "signal-exit": "^3.0.2" 126 | } 127 | }, 128 | "signal-exit": { 129 | "version": "3.0.2", 130 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 131 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 132 | }, 133 | "strip-ansi": { 134 | "version": "5.2.0", 135 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 136 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 137 | "requires": { 138 | "ansi-regex": "^4.1.0" 139 | } 140 | }, 141 | "supports-color": { 142 | "version": "5.5.0", 143 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 144 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 145 | "requires": { 146 | "has-flag": "^3.0.0" 147 | } 148 | }, 149 | "wcwidth": { 150 | "version": "1.0.1", 151 | "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", 152 | "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", 153 | "requires": { 154 | "defaults": "^1.0.3" 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /spinners-sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spinners-sample", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@types/ora": "4.0.2", 6 | "ora": "^4.0.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spinners-sample/src/main/java/net/plan99/nodejs/sample/spinners/java/SpinnerJavaDemo.java: -------------------------------------------------------------------------------- 1 | package net.plan99.nodejs.sample.spinners.java; 2 | 3 | import net.plan99.nodejs.java.NodeJS; 4 | import org.graalvm.polyglot.TypeLiteral; 5 | import org.graalvm.polyglot.Value; 6 | 7 | public class SpinnerJavaDemo { 8 | interface Ora { 9 | void start(String text); 10 | } 11 | public static void main(String[] args) throws InterruptedException { 12 | NodeJS.executor.execute(() -> { 13 | Value v = NodeJS.eval("require('ora')()"); 14 | Ora ora = NodeJS.castValue(v, new TypeLiteral() {}); 15 | ora.start("Hello world!"); 16 | }); 17 | Thread.sleep(2000); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spinners-sample/src/main/kotlin/net/plan99/nodejs/sample/spinners/SpinnerDemo.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Demo of using the 'ora' module, which generates pretty terminal spinners. Translated from 3 | * this JavaScript code sample: 4 | * 5 | * ``` 6 | * const ora = require('ora'); 7 | * 8 | * const spinner = ora('Loading unicorns').start(); 9 | * 10 | * setTimeout(() => { 11 | * spinner.color = 'yellow'; 12 | * spinner.text = 'Loading rainbows'; 13 | * }, 1000); 14 | * ``` 15 | * 16 | */ 17 | package net.plan99.nodejs.sample.spinners 18 | 19 | import net.plan99.nodejs.kotlin.nodejs 20 | 21 | interface Ora { 22 | fun start(text: String) 23 | fun info(text: String) 24 | fun warn(text: String) 25 | fun error(text: String) 26 | 27 | var text: String 28 | var prefixText: String? 29 | var color: String 30 | } 31 | 32 | fun main() { 33 | val spinner: Ora = nodejs { eval("require('ora')()") } 34 | 35 | nodejs { 36 | spinner.start("Loading unicorns ...") 37 | } 38 | 39 | // We don't want to block the nodeJS thread by sleeping inside a nodejs{} block. 40 | // This sleep represents some sort of "work". 41 | Thread.sleep(2000) 42 | 43 | // Change color and message. 44 | nodejs { 45 | spinner.color = "red" 46 | spinner.text = "Loading rainbows" 47 | spinner.prefixText = "Working" 48 | } 49 | 50 | // "Work" a bit more. 51 | Thread.sleep(3000) 52 | 53 | // Show a nice "info" message instead. 54 | nodejs { 55 | spinner.prefixText = null 56 | spinner.info("Info!") 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/Demo.java: -------------------------------------------------------------------------------- 1 | import net.plan99.nodejs.java.NodeJS; 2 | 3 | public class Demo { 4 | public static void main(String[] args) { 5 | int result = NodeJS.runJS(() -> 6 | NodeJS.eval("return 2 + 3 + 4").asInt() 7 | ); 8 | System.out.println(result); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/net/plan99/nodejs/java/NodeJS.java: -------------------------------------------------------------------------------- 1 | package net.plan99.nodejs.java; 2 | 3 | import net.plan99.nodejs.kotlin.NodeJSAPI; 4 | import org.graalvm.polyglot.Context; 5 | import org.graalvm.polyglot.PolyglotException; 6 | import org.graalvm.polyglot.TypeLiteral; 7 | import org.graalvm.polyglot.Value; 8 | import org.intellij.lang.annotations.Language; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.lang.reflect.InvocationTargetException; 15 | import java.lang.reflect.Method; 16 | import java.lang.reflect.Modifier; 17 | import java.net.URL; 18 | import java.net.URLClassLoader; 19 | import java.nio.file.Files; 20 | import java.nio.file.Paths; 21 | import java.nio.file.StandardOpenOption; 22 | import java.util.Arrays; 23 | import java.util.concurrent.CompletableFuture; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.Executor; 26 | import java.util.concurrent.LinkedBlockingDeque; 27 | import java.util.function.Consumer; 28 | import java.util.function.Supplier; 29 | import java.util.jar.JarInputStream; 30 | 31 | /** 32 | * Provides an interface to the NodeJS runtime for Java developers. You can only access the NodeJS world 33 | * when running on the event loop thread, which means you must use the various runJS methods on this class 34 | * to get onto the correct thread before using eval. 35 | */ 36 | @SuppressWarnings("WeakerAccess") 37 | public class NodeJS { 38 | private static class Linkage { 39 | final Value throwFunction; 40 | final LinkedBlockingDeque taskQueue; 41 | final Value evalFunction; 42 | final Thread nodeJSThread; 43 | final Context ctx; 44 | 45 | Linkage(LinkedBlockingDeque taskQueue, Value evalFunction, Value throwFunction) { 46 | this.taskQueue = taskQueue; 47 | this.evalFunction = evalFunction; 48 | this.throwFunction = throwFunction; 49 | this.nodeJSThread = Thread.currentThread(); 50 | this.ctx = Context.getCurrent(); 51 | } 52 | } 53 | 54 | private volatile static Linkage linkage; 55 | 56 | // Called from the boot.js file as part of NodeJVM startup, do not call. 57 | @SuppressWarnings("unused") 58 | @Deprecated 59 | public static void boot(LinkedBlockingDeque taskQueue, 60 | Value evalFunction, 61 | Value throwFunction, 62 | String[] args) { 63 | try { 64 | boot1(taskQueue, evalFunction, throwFunction, args); 65 | } catch (Throwable e) { 66 | e.printStackTrace(); 67 | System.exit(1); 68 | } 69 | } 70 | 71 | private static void boot1(LinkedBlockingDeque taskQueue, Value evalFunction, Value throwFunction, String[] args) throws ClassNotFoundException, IOException { 72 | assert linkage == null : "Don't call this function directly. Already started!"; 73 | assert evalFunction.canExecute(); 74 | NodeJS.linkage = new Linkage(taskQueue, evalFunction, throwFunction); 75 | Thread.currentThread().setName("NodeJS main thread"); 76 | 77 | if (args.length == 0) { 78 | System.err.println("You must specify at least a class name, or -jar jarname.jar"); 79 | System.exit(1); 80 | } else if (!args[0].equals("-jar")) { 81 | Class entryPoint = Class.forName(args[0]); 82 | startJavaThread(entryPoint, Arrays.copyOfRange(args, 1, args.length)); 83 | } else { 84 | File myJar = new File(args[1]); 85 | final URL url = myJar.toURI().toURL(); 86 | String mainClassName; 87 | try (InputStream stream = Files.newInputStream(Paths.get(args[1]), StandardOpenOption.READ)) { 88 | JarInputStream jis = new JarInputStream(stream); 89 | mainClassName = jis.getManifest().getMainAttributes().getValue("Main-Class"); 90 | } 91 | 92 | if (mainClassName == null) { 93 | System.err.println("JAR file does not have a Main-Class attribute, is not executable."); 94 | System.exit(1); 95 | } 96 | // Use the parent classloader to forcibly toss out this version of the interop JAR, to avoid confusion 97 | // later when there are two classloaders in play, BUT, holepunch this specific class and inner classes 98 | // through so the state linked up from the bootstrap script is still here. In other words, this class is 99 | // special, so don't give it dependencies outside the JDK. 100 | ClassLoader thisClassLoader = NodeJS.class.getClassLoader(); 101 | URLClassLoader child = new URLClassLoader(new URL[] {url}, thisClassLoader.getParent()) { 102 | @Override 103 | protected Class findClass(String name) throws ClassNotFoundException { 104 | if (name.startsWith(NodeJS.class.getName())) { 105 | return thisClassLoader.loadClass(name); 106 | } else 107 | return super.findClass(name); 108 | } 109 | }; 110 | Class entryPoint = Class.forName(mainClassName, true, child); 111 | startJavaThread(entryPoint, Arrays.copyOfRange(args, 2, args.length)); 112 | } 113 | } 114 | 115 | private static void startJavaThread(Class entryPoint, String[] args) { 116 | Thread javaThread = new Thread(() -> { 117 | try { 118 | Method main = entryPoint.getMethod("main", String[].class); 119 | assert Modifier.isStatic(main.getModifiers()); 120 | main.invoke(null, new Object[] { args }); 121 | System.exit(0); 122 | } catch (NoSuchMethodException e) { 123 | System.err.println("No main method found in " + entryPoint.getName()); 124 | } catch (InvocationTargetException e) { 125 | e.getCause().printStackTrace(); 126 | } catch (Throwable e) { 127 | e.printStackTrace(); 128 | } 129 | System.exit(1); 130 | }, "Java main thread"); 131 | javaThread.start(); 132 | } 133 | 134 | /** 135 | * Returns true if executing within the main NodeJS thread. Note: does NOT return true if you are executing on 136 | * some other worker you created yourself. 137 | */ 138 | public static boolean isOnMainNodeThread() { 139 | return Thread.currentThread() == linkage.nodeJSThread; 140 | } 141 | 142 | /** 143 | * Throws {@link IllegalStateException} if you aren't on the NodeJS main thread. 144 | */ 145 | public static void checkOnMainNodeThread() { 146 | if (!isOnMainNodeThread()) 147 | throw new IllegalStateException("You are not currently on the NodeJS thread and thus cannot access the JavaScript world. Surround your calls to JS with NodeJS.runJS(), NodeJS.runJSAsync() or the Kotlin nodejs{} block."); 148 | } 149 | 150 | /** 151 | * This {@link Executor} runs the given commands on the NodeJS event loop thread. The other utility functions 152 | * on this class are all just utilities that delegate to this executor. 153 | */ 154 | public static Executor executor = new Executor() { 155 | @Override 156 | public void execute(@NotNull Runnable command) { 157 | if (linkage == null) 158 | throw new IllegalStateException("This JVM was not started with the nodejvm script."); 159 | 160 | if (Thread.currentThread() == linkage.nodeJSThread) 161 | command.run(); 162 | else 163 | linkage.taskQueue.add(command); 164 | } 165 | }; 166 | 167 | /** 168 | * Schedules execution of the given {@link Supplier} onto the NodeJS thread, and returns a future that will 169 | * complete when it's been run. 170 | */ 171 | public static CompletableFuture runJSAsync(Supplier callable) { 172 | return CompletableFuture.supplyAsync(callable, executor); 173 | } 174 | 175 | /** 176 | * Returns a future that completes when the given promise completes. 177 | * 178 | * @param promise Something that is 'thenable', i.e. conforms to the JS promise protocol. 179 | * @return A future that completes when the 'then' handler is invoked by the promise. 180 | */ 181 | public static CompletableFuture futureFromPromise(Value promise) { 182 | var f = new CompletableFuture(); 183 | promise.invokeMember( 184 | "then", 185 | (Consumer) it -> { 186 | //noinspection resource 187 | f.complete(NodeJS.polyglotContext().asValue(it)); 188 | }, 189 | (Consumer) it -> { 190 | System.err.println("Promise is resolving with error:\n" + it); 191 | try { 192 | NodeJS.linkage.throwFunction.execute(it); 193 | } catch (PolyglotException ex) { 194 | ex.printStackTrace(); 195 | f.completeExceptionally(ex); 196 | } 197 | } 198 | ); 199 | return f; 200 | } 201 | 202 | /** 203 | * Runs the given {@link Supplier} on the NodeJS thread, blocks until execution has been performed and 204 | * returns the result that was computed. 205 | */ 206 | public static T runJS(Supplier callable) { 207 | try { 208 | return runJSAsync(callable).get(); 209 | } catch (InterruptedException e) { 210 | throw new RuntimeException(e); 211 | } catch (ExecutionException e) { 212 | if (e.getCause() instanceof RuntimeException) 213 | throw (RuntimeException) e.getCause(); 214 | else 215 | throw new RuntimeException(e.getCause()); 216 | } 217 | } 218 | 219 | /** 220 | * Evaluates the given string and returns a generic {@link Value} object representing the result, that can then 221 | * be converted into more useful forms. Note that you must be on the NodeJS thread for this to work (see 222 | * {@link #runJS(Supplier)} for how to do this). 223 | * 224 | * @throws IllegalStateException if you're not on the NodeJS thread. 225 | */ 226 | public static Value eval(@Language("JavaScript") String js) { 227 | checkOnMainNodeThread(); 228 | return linkage.evalFunction.execute(js); 229 | } 230 | 231 | /** 232 | * Returns the {@link Context} for the NodeJS main thread. 233 | */ 234 | public static Context polyglotContext() { 235 | return linkage.ctx; 236 | } 237 | 238 | /** 239 | * Converts the {@link Value} to a JVM type in the following way:

240 | * 241 | *

    242 | *
  1. If the type is an interface not annotated with {@code @FunctionalInterface} then a special proxy is returned that 243 | * knows how to map JavaBean style property methods on that interface to JavaScript properties.
  2. 244 | *
  3. Otherwise, the {@link Value#as} method is used with the {@link TypeLiteral} so generics are preserved and 245 | * the best possible translation occurs.
  4. 246 | *
247 | */ 248 | public static T castValue(Value value, TypeLiteral typeLiteral) { 249 | return NodeJSAPI.castValue(value, typeLiteral); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/net/plan99/nodejs/kotlin/NodeJS.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_VARIABLE") 2 | 3 | package net.plan99.nodejs.kotlin 4 | 5 | import net.plan99.nodejs.java.NodeJS 6 | import org.graalvm.polyglot.TypeLiteral 7 | import org.graalvm.polyglot.Value 8 | import org.intellij.lang.annotations.Language 9 | import java.lang.reflect.InvocationHandler 10 | import java.lang.reflect.Method 11 | import java.lang.reflect.Proxy 12 | import kotlin.reflect.KProperty 13 | 14 | /** 15 | * Enters the NodeJS event loop thread and makes the JavaScript API available in scope. You can return JavaScript 16 | * objects from a nodejs block, however, you can't access them again until you're back inside. Attempting to use 17 | * them outside a nodejs block will trigger a threading violation exception. 18 | * 19 | * Note that this function schedules the lambda onto the NodeJS event loop and waits for it to be executed. If Node 20 | * is busy, it'll block and wait for it. 21 | */ 22 | fun nodejs(body: NodeJSAPI.() -> T): T = NodeJS.runJS { body(NodeJSAPI()) } 23 | 24 | /** 25 | * The API for working with the NodeJS world. Accessed by using a [nodejs] block: the methods on this class are in 26 | * scope when inside the code block. 27 | */ 28 | class NodeJSAPI internal constructor() { 29 | /** 30 | * Converts the [Value] to a JVM type [T] in the following way: 31 | * 32 | * 1. If the type is an interface not annotated with `@FunctionalInterface` then a special proxy is returned that 33 | * knows how to map JavaBean style property methods on that interface to JavaScript properties. 34 | * 2. Otherwise, the [Value. as] method is used with a [TypeLiteral] so generics are preserved and the best possible 35 | * translation occurs. 36 | */ 37 | inline fun Value.cast(): T = castValue(this, object : TypeLiteral() {}) 38 | 39 | companion object { 40 | /** @suppress */ 41 | @JvmStatic 42 | fun castValue(value: Value, typeLiteral: TypeLiteral): T { 43 | val clazz = typeLiteral.rawType 44 | @Suppress("UNCHECKED_CAST") 45 | return if (JSTranslationProxyHandler.isTranslateableInterface(clazz)) 46 | Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz, *clazz.interfaces), JSTranslationProxyHandler(value)) as T 47 | else 48 | value.`as`(typeLiteral) 49 | } 50 | } 51 | 52 | /** Casts any object to being a JavaScript object. */ 53 | fun Any.asValue(): Value = NodeJS.polyglotContext().asValue(this) 54 | 55 | /** 56 | * Evaluates the given JavaScript string and casts the result to the desired JVM type. You can request a cast 57 | * to interfaces that map to JS objects, collections, the Graal/Polyglot [Value] type, boxed primitives and more. 58 | */ 59 | inline fun eval(@Language("JavaScript") javascript: String): T = NodeJS.eval(javascript).cast() 60 | 61 | /** 62 | * Evaluates the given JavaScript but throws away any result. 63 | */ 64 | fun run(@Language("JavaScript") javascript: String) { 65 | NodeJS.eval(javascript) 66 | } 67 | 68 | /** Allows you to read JS properties of the given [Value] using Kotlin indexing syntax. */ 69 | inline operator fun Value.get(key: String): T = getMember(key).cast() 70 | 71 | /** Allows you to read JS properties of the given [Value] using Kotlin indexing syntax. */ 72 | operator fun Value.get(key: String): Value = getMember(key) 73 | 74 | /** Allows you to set JS properties of the given [Value] using Kotlin indexing syntax. */ 75 | operator fun Value.set(key: String, value: Any?) = putMember(key, value) 76 | 77 | private val bindings = NodeJS.polyglotContext().polyglotBindings 78 | 79 | /** 80 | * Implementation for [bind]. The necessary operator functions are defined as extensions to allow for reified generics. 81 | * @suppress 82 | */ 83 | class Binding 84 | 85 | /** @suppress */ 86 | operator fun Binding.setValue(thisRef: Any?, property: KProperty<*>, value: T) { 87 | // This rather ugly hack is required as we can't just insert the name directly, 88 | // we have to go via an intermediate 'bindings' map. 89 | bindings["__nodejvm_transfer"] = value 90 | NodeJS.eval("${property.name} = Polyglot.import('__nodejvm_transfer');") 91 | bindings.removeMember("__nodejvm_transfer") 92 | } 93 | 94 | /** @suppress */ 95 | inline operator fun Binding.getValue(thisRef: Any?, property: KProperty<*>): T = eval(property.name) 96 | 97 | /** @suppress */ 98 | inner class Binder(private val default: T? = null) { 99 | operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): Binding { 100 | val b = Binding() 101 | if (default != null) 102 | b.setValue(null, prop, default) 103 | return b 104 | } 105 | } 106 | 107 | /** 108 | * Use this in property delegate syntax to access top level global variables in the NodeJS context. By declaring 109 | * a variable as `var x: String by bind()` you can read and write the 'x' global variable in JavaScript world. 110 | */ 111 | fun bind(default: T? = null) = Binder(default) 112 | } 113 | 114 | /** Wraps JS objects with some Bean property convenience glue. */ 115 | private class JSTranslationProxyHandler(private val value: Value) : InvocationHandler { 116 | companion object { 117 | fun isTranslateableInterface(c: Class<*>) = 118 | c.isInterface && !c.isAnnotationPresent(FunctionalInterface::class.java) 119 | } 120 | 121 | // This code does a lot of redundant work on every method call and could be optimised with caches. 122 | init { 123 | check(nodejs { value.hasMembers() }) { "Cannot translate this value to an interface because it has no members." } 124 | } 125 | 126 | override fun invoke(proxy: Any, method: Method, args: Array?): Any? { 127 | // Apply Bean-style naming pattern matching. 128 | val name = method.name 129 | fun hasPropName(p: Int) = name.length > p && name[p].isUpperCase() 130 | val getter = name.startsWith("get") && hasPropName(3) 131 | val setter = name.startsWith("set") && hasPropName(3) 132 | val izzer = name.startsWith("is") && hasPropName(2) 133 | val isPropAccess = getter || setter || izzer 134 | val propName = if (isPropAccess) { 135 | if (getter || setter) { 136 | name.drop(3).decapitalize() 137 | } else { 138 | check(izzer) 139 | name.drop(2).decapitalize() 140 | } 141 | } else null 142 | 143 | val returnType = method.returnType 144 | val parameterCount = method.parameterCount 145 | 146 | when { 147 | izzer -> check(returnType == Boolean::class.java && parameterCount == 0) { 148 | "Methods starting with 'is' should return boolean and have no parameters." 149 | } 150 | getter -> check(parameterCount == 0) { "Methods starting with 'get' should not have any parameters." } 151 | setter -> check(parameterCount == 1) { "Methods starting with 'set' should have a single parameter." } 152 | } 153 | 154 | NodeJS.checkOnMainNodeThread() 155 | 156 | return if (propName != null) { 157 | if (getter || izzer) { 158 | val member = value.getMember(propName) 159 | ?: throw IllegalStateException("No property with name $propName found: [${value.memberKeys}] ") 160 | member.`as`(returnType) 161 | } else { 162 | check(setter) 163 | value.putMember(propName, args!!.single()) 164 | null 165 | } 166 | } else { 167 | // Otherwise treat it as a method call. 168 | check(value.canInvokeMember(name)) { "Method $name does not appear to map to an executable member: [${value.memberKeys}]" } 169 | val result = value.invokeMember(name, *(args ?: emptyArray())) 170 | // The result should be thrown out if expecting void, or translated again if the return type is a 171 | // non-functional interface (functional interfaces are auto-translated by Polyglot already), or 172 | // otherwise we just rely on the default Polyglot handling which is pretty good most of the time. 173 | when { 174 | returnType == Void.TYPE -> null 175 | isTranslateableInterface(returnType) -> Proxy.newProxyInstance( 176 | this.javaClass.classLoader, 177 | returnType.interfaces, 178 | JSTranslationProxyHandler(result) 179 | ) 180 | else -> result.`as`(returnType) 181 | } 182 | } 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /src/main/resources/boot.js: -------------------------------------------------------------------------------- 1 | // Set up a task queue that we'll proxy onto the NodeJS main thread. 2 | // 3 | // We have to do it this way because NodeJS/V8 are not thread safe, 4 | // and JavaScript has no shared memory concurrency support, only 5 | // message passing. It's like Visual Basic 6 all over again except 6 | // this time without DCOM to wallpaper over what's really happening. 7 | // 8 | // Fortunately, we can call Java objects from JS and those CAN be 9 | // shared memory. So we create an intermediate JS thread here that 10 | // will spend its time blocked waiting for lambdas to be placed on 11 | // the queue. Then it'll transmit them to the main event loop for 12 | // execution. 13 | let javaToJSQueue = new java.util.concurrent.LinkedBlockingDeque(); 14 | const { Worker } = require('worker_threads'); 15 | 16 | let worker = new Worker(` 17 | const { workerData, parentPort } = require('worker_threads'); 18 | while (true) { 19 | parentPort.postMessage(workerData.take()); 20 | } 21 | `, { eval: true, workerData: javaToJSQueue }); 22 | 23 | worker.on('message', (callback) => { 24 | try { 25 | callback(); 26 | } catch (e) { 27 | console.log(e); 28 | } 29 | }); 30 | 31 | // We need this wrapper around eval because GraalJS barfs if we try to call eval() directly from Java context, it assumes 32 | // it will only ever be called from JS context. 33 | Java.type('net.plan99.nodejs.java.NodeJS').boot( 34 | javaToJSQueue, 35 | function(str) { return eval(str); }, 36 | function(err) { throw err; }, 37 | process.env['ARGS'].split(" ") 38 | ); 39 | -------------------------------------------------------------------------------- /src/main/resources/nodejvm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check that we have the GraalVM node binary on our path, not normal node. 4 | if [[ "$GRAALVM_PATH" != "" ]]; then 5 | node="$GRAALVM_PATH/bin/node" 6 | else 7 | node=`which node` 8 | fi 9 | 10 | if [[ "$node" == "" ]]; then 11 | echo "No node binary found on your path. Consider setting \$GRAALVM_PATH to the bin directory of your GraalVM install." 12 | exit 1 13 | fi 14 | 15 | $node --version:graalvm >/dev/null 2>/dev/null 16 | if [[ $? != 0 ]]; then 17 | echo "Running 'node --version:graalvm' failed." 18 | echo "Make sure the '$node' binary on your \$PATH is the GraalVM version of node." 19 | echo "Alternatively set the environment variable \$GRAALVM_PATH to the bin directory." 20 | exit 1 21 | fi 22 | 23 | # Gradle likes to probe the JVM version by running it this way, however the nodejs hybrid vm doesn't respond in the 24 | # same way so we provide the expected answer here. 25 | if [[ "$1" == "-version" ]]; then 26 | `dirname $node`/java -version 27 | exit 0 28 | fi 29 | 30 | # Figure out our current directory. 31 | nodejvmDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd ) 32 | interopJar="$nodejvmDir/nodejs-interop-@ver@.jar" 33 | 34 | if [[ ! -e "$interopJar" ]]; then 35 | echo "$interopJar not found, is this script installed correctly?" 36 | exit 1 37 | fi 38 | 39 | # Mangle the command line arguments. We expect to receive a normal JVM command line 40 | # and want to convert it into a GraalJS node command line, which requires adding flags, 41 | # editing flag names, inserting options, etc. 42 | export NODE_JVM_OPTIONS="" 43 | declare -a args 44 | args=( $@ ) 45 | argCount=${#args[@]} 46 | i=0 47 | while (( i < argCount )); do 48 | arg="${args[i]}" 49 | case "$arg" in 50 | -cp|-classpath|--classpath) 51 | NODE_JVM_CLASSPATH="${args[i+1]}" 52 | (( i++ )) 53 | ;; 54 | --*|-*) 55 | if [[ "$arg" == "-jar" ]]; then break; fi; 56 | NODE_JVM_OPTIONS="$NODE_JVM_OPTIONS $arg" 57 | ;; 58 | *) 59 | break;; 60 | esac 61 | (( i++ )) 62 | done 63 | args=${args[@]:$i} 64 | 65 | # Make node look for modules in the executed-from directory in the same way 66 | # it would if run from the command line normally. 67 | export NODE_PATH=$PWD/node_modules 68 | 69 | # Now feed the boot.js script to the node binary. The quoted 'EOF' here 70 | # doc string disables parsing and interpolation of the embedded script, 71 | # which is needed because backticks are meaningful to both JS and shell. 72 | read -r -d '' BOOTJS <<'EOF' 73 | @bootjs@ 74 | EOF 75 | 76 | export BOOTJS="$BOOTJS" 77 | export ARGS="$args" 78 | export NODE_JVM_CLASSPATH="$interopJar:$NODE_JVM_CLASSPATH" 79 | 80 | cmd="$node --experimental-worker --jvm -e eval(process.env['BOOTJS'])" 81 | exec $cmd 82 | --------------------------------------------------------------------------------