We’ve been using Nashorn for a while to execute some very simple Javascript expressions. The latest challenge was to run some JavaScript from a Node.js project, which in turn had dependencies on a third party package. There were a few gotchas which I thought I’d share (disclaimer: I’m not a JavaScript developer, so this might all be obvious stuff to some).
The original JavaScript was in a plain old text file, with functions at the top level
function testThing(thing) {
return thing === 'tiger';
}
This is how I was executing it:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String js = IOUtils.toString(this.getClass().getClassLoader().getResource("my-script.js"), "UTF-8");
engine.eval(js);
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("testThing", theThing);
Running Javascript generated from a Node.js project required a bit of tinkering:
1. In Java 8 Nashorn will only load ES5 compatible Javascript
I was running browserify to create a single JavaScript file from the Node.js project. Trying to load the generated instead of the plain javascript failed miserably:
javax.script.ScriptException: :1079:0 Expected : but found }
To get Nashorn to evaluate the js file, I had to transpile to ES5 using babel.
I decided I needed to run browsify in standalone mode, so that my exported functions were attached to a global variable. The scripts section in package.json had the following:
"build": "browserify src/main.js -r --standalone MyLib -o dist/build.js",
"transpile": "babel dist/build.js --out-file dist/build.es5.js"
This was a small step forward but Nashorn was still not able to load the script successfully…
2. Nashorn cannot set property of undefined!
Loading the transpiled file in Nashorn threw the following:
javax.script.ScriptException: TypeError: Cannot set property “MyLib” of undefined in <eval> at line number 19
Looking at the beginning of the transpiled file, you can see it is trying to determine where would be suitable to attach the global variable:
(function (f) {
if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object" && typeof module !== "undefined") {
module.exports = f();
} else if (typeof define === "function" && define.amd) {
define([], f);
} else {
var g;if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
g = this;
}
g.MyLib = f();
}
})
A little bit of detective work using print() showed that it was falling through to the final “else” as it couldn’t find anything else to attach to. Adding this line to the Java, before parsing the file, magicked away this problem.
engine.eval("var global = this;");
(It doesn’t quite make sense to me why this works, as loading the script without this line it seems to think “this” is undefined?)
3. Executing methods of objects is different from executing functions
At this point, the file was loading without an error, but trying to execute the function using the original Java code with the new JavaScript file threw a NoSuchMethodException
java.lang.NoSuchMethodException: No such function testThing
This makes sense, as “testThing” is now a method of the MyLib global variable. However none of the permutations I tried in order to access the function worked. I tried all sorts, e.g:
Object result = invocable.invokeFunction("global.MyLib.testThing", theThing);
The key issue here is that testThing isn’t a function now, it is a method on an object, so we have to use invokeMethod:
Object result = invocable.invokeMethod(engine.eval("global.MyLib"), "testThing", theThing);
And that worked :)
Not sure if any of this is the correct way to approach executing Node.js based Javascript on the JVM, but these were the three gotchas that took me a while to work out.