JavaScript engines
Contents
Server-side code in an Enonic app runs on an embedded JavaScript engine inside the JVM - see Runtime. Two engines are available: Nashorn, the default, and GraalJS, available as a preview since XP 8.1.0.
Choosing an engine
The engine is chosen per application. Declare it in build.gradle:
app {
scriptEngine = 'GraalJS'
}
The value becomes the X-Script-Engine header in the application’s bundle manifest, and the app’s tests run on the same engine. An application that declares no engine runs on the installation default, set by xp.script-engine in the platform’s system properties. The default is Nashorn.
Applications on different engines coexist in the same installation, so a single app can be moved to GraalJS and back without touching the rest.
Every module runs in ECMAScript strict mode, on both engines. Nothing about how you write an app changes with the engine: the same modules, the same libraries, the same Java bridge. What changes is the language level and the execution environment around them.
Nashorn
Nashorn is the default and the supported engine, shipped with every XP release since 7.x. It implements ECMAScript 5.1, with only a subset of later (ES2015+) features. Target ES5 when compiling TypeScript for Nashorn. See ECMAScript target for build configuration.
GraalJS
GraalJS is the engine that will replace Nashorn. It supports modern ECMAScript, so the build can preserve newer JavaScript syntax. This does not add native TypeScript support: TypeScript must still be compiled to JavaScript before deployment. See ECMAScript target.
| GraalJS is not for production use. Nashorn remains the supported engine, and what follows may change while GraalJS is in preview. |
ES5 remains a common syntax target when supporting both engines. An app must also account for the runtime and Java interoperability differences described below when switching engines.
Detecting the engine
GraalJS installs a Graal global, and Nashorn has no such name:
if (typeof Graal !== 'undefined') {
log.info(`${Graal.language} ${Graal.versionECMAScript}`); // JavaScript 2026
}
Graal.versionECMAScript is the year of the ECMAScript compatibility mode in force. Graal.versionGraalVM gives the GraalVM version. See the Graal object in the GraalJS compatibility reference.
Not supported
A current-standard engine brings browser and Node.js expectations with it. XP is a different environment:
- ES modules
-
Not supported as a loading mechanism. XP loads every file as a CommonJS module, wrapped in a function, on both engines. GraalJS can parse
importandexport, but a file XP loads is never evaluated as an ES module, so those statements fail at runtime. Write them in TypeScript and let the build transpile them; see ECMAScript modules. -
async/await -
Not supported. Write synchronous code, and move work that should not block a request into a task.
- Node.js APIs
-
Not supported. No
process,Bufferorglobal, and nonode_modulesresolution. - Web APIs
-
Partially supported.
TextEncoderandTextDecoderare available. Further APIs may become available in future GraalJS versions, while others, such asfetchandsetTimeout, might never be supported. For HTTP, mail, storage and the rest, use the platform libraries.
Java interop
The Java bridge is identical on both engines: the same bridge object, the same beans, the same libraries. What differs is conversion. Nashorn converted values implicitly as they crossed between JavaScript and Java, whereas GraalJS converts nothing. Four rules follow from that, and code written to observe them runs unchanged on either engine.
Convert values at the boundary
Values are converted explicitly: on the way into Java with toScriptValue, and on the way back out with toNativeObject.
const bean = __.newBean<MyBean>('com.example.MyBean');
bean.setConfig(__.toScriptValue(params.config));
const result = __.toNativeObject(bean.execute());
A Java map returned without conversion remains a Java object. Inspecting it with the standard JavaScript operations returns the methods of its class instead of its entries, and throws nothing along the way. An individual property read may still return the expected value, which is what makes the omission easy to overlook.
const map = bean.getConfig(); // a Java Map, unconverted
const mode = map.mode; // 'live' - the read succeeds
const keys = Object.keys(map); // ['get', 'put', 'size', ...]
const json = JSON.stringify(map); // '{}'
const copy = {...map}; // {}
const config = __.toNativeObject(bean.getConfig()); // a JavaScript object
Java lists are the exception. They present array semantics on both engines, so index access and iteration require no conversion.
Call setters rather than assigning properties
Nashorn accepted an assignment to a property named after the setter and rewrote it into a call to that setter. GraalJS exposes the bean’s methods and its public fields, and nothing else, so no such property exists and the assignment fails with an Unknown identifier error.
bean.setValue(42); // supported on both engines
bean.value = 42; // Nashorn only; fails on GraalJS
TypeScript declarations written for a bean should therefore declare its setters and omit the properties, so that the unsupported form does not compile.
Pass the type the method declares
Arguments are not coerced to the declared parameter type. HTTP request parameters, for instance, always arrive as strings and must be parsed before they reach a Java method that expects a number.
bean.setCount(req.params.count); // fails: a string is not an int
bean.setCount(parseInt(req.params.count, 10)); // supported on both engines
Do not determine Java types from JavaScript
The standard type-tag idiom reported the name of the Java class on Nashorn. On GraalJS it reports a generic tag, so a branch written against that string stops matching, and it does so silently.
Object.prototype.toString.call(value);
// Nashorn: '[object com.example.Thing]'
// GraalJS: '[object Object]'
Convert the value at the boundary and branch on the converted result, or perform the type test in Java, where the type is known.
The bridge XP supports is the __ object, on both engines. Interop globals installed by an engine itself, among them the Java object, are not part of the XP API, and code written against them is not portable between engines. |
GraalJS is documented by its maintainers at graalvm.org. That reference describes the engine as it is distributed standalone; XP embeds it, so it should be read together with Not supported above. What the engine is capable of and what an application may rely on are not the same set.
Script contexts
GraalJS executes an application in script contexts. The rules below follow from that, and they are stricter versions of what threading already requires.
- An application gets a pool of contexts
-
Contexts are created lazily as concurrency demands them, and module-level state is per context. Two requests may load the same module and see two different copies of its top-level variables. Treat top-level state as read-only, and put anything that must be shared in a repo node, the cache library, or another explicit store.
- Compiled code is shared
-
One engine serves the whole installation, and the code it compiles is cached across every context built from it. A module parsed while one context starts is reused by the next.
- Values cannot leave their context
-
A JavaScript function or object belongs to the context that created it.
executeFunction()is therefore unsupported. Submit a named task withsubmitTask()instead. - Disposers are registered during start-up
-
Register
__.disposerfrommain.ts, or from a module it loads while starting. See__.disposer.
The size of the context pool is tuned in the platform’s system properties. The defaults normally need no attention.
Trying it out
Run your test suite against both engines before switching an app. The differences above are the ones the platform imposes; a dependency written against Nashorn’s language level can have its own. With app { scriptEngine } set, the Gradle plugin runs the app’s tests on that engine.