From 4e3973a0b5e1565829cee73fb22e98f5ff3177f1 Mon Sep 17 00:00:00 2001 From: john spurling Date: Thu, 4 Jun 2026 00:53:45 +0000 Subject: [PATCH] Add GraalJS engine support alongside Nashorn Add support for the GraalJS JavaScript engine as an alternative to Nashorn, allowing the library to use whichever engine is available at runtime. GraalJS is tried first, falling back to Nashorn. GraalJS is configured with three polyglot options: - polyglot.js.allowAllAccess=true: enables Java.type() interop needed by helpers.nashorn.js to bridge Java classes into JavaScript. - polyglot.js.nashorn-compat=true: enables Nashorn-like bean property access so that `bean.name` in JS resolves to `getName()` on Java objects, which the existing helper bridge relies on. - polyglot.js.ecmascript-version=2022: overrides the ES5 default imposed by nashorn-compat mode, allowing ES6+ syntax (let, const, arrow functions) in user-supplied helper scripts. The adaptES6Literals() method in DefaultHelperRegistry, which converts let/const to var for Nashorn's ES5 parser, is now only applied when actually running on Nashorn. GraalJS handles ES6+ natively with the ecmascript-version override. Both engine dependencies (nashorn-core, js-scriptengine + polyglot js) are marked true in handlebars/pom.xml so consumers choose which engine to include. The handlebars-maven-plugin adds an explicit nashorn-core dependency since it needs a concrete engine for template precompilation. To run tests with a specific engine: - GraalJS only: -Dmaven.test.classpathDependencyExcludes=org.openjdk.nashorn:nashorn-core - Nashorn only: -Dmaven.test.classpathDependencyExcludes=org.graalvm.js:js-scriptengine --- handlebars-maven-plugin/pom.xml | 6 ++ handlebars/pom.xml | 68 ++++++++++++ .../github/jknack/handlebars/Handlebars.java | 18 ++-- .../helper/DefaultHelperRegistry.java | 10 +- .../internal/ScriptEngineFactory.java | 101 ++++++++++++++++++ .../github/jknack/handlebars/NashornTest.java | 8 +- 6 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java diff --git a/handlebars-maven-plugin/pom.xml b/handlebars-maven-plugin/pom.xml index 2e6693e8..6933104e 100644 --- a/handlebars-maven-plugin/pom.xml +++ b/handlebars-maven-plugin/pom.xml @@ -21,6 +21,12 @@ ${project.version} + + org.openjdk.nashorn + nashorn-core + 15.4 + + ch.qos.logback logback-classic diff --git a/handlebars/pom.xml b/handlebars/pom.xml index 581a14e8..f194c83a 100644 --- a/handlebars/pom.xml +++ b/handlebars/pom.xml @@ -170,6 +170,23 @@ org.openjdk.nashorn nashorn-core ${nashorn.version} + true + + + + org.graalvm.js + js-scriptengine + ${graaljs.version} + true + + + + org.graalvm.polyglot + js + ${graaljs.version} + pom + runtime + true @@ -240,6 +257,7 @@ 15.4 + 25.0.3 @@ -300,5 +318,55 @@ + + + graaljs-jit + + + org.graalvm.compiler + compiler + ${graaljs.version} + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + graal-compiler-path + initialize + + properties + + + + copy-graaljs-modules + generate-test-resources + + copy-dependencies + + + ${project.build.directory}/graaljs-modules + org.graalvm.polyglot,org.graalvm.js,org.graalvm.truffle,org.graalvm.sdk,org.graalvm.regex,org.graalvm.shadowed + compiler + pom + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + -Duser.language=en -Duser.country=US -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --module-path=${project.build.directory}/graaljs-modules --add-modules=org.graalvm.polyglot --upgrade-module-path=${org.graalvm.compiler:compiler:jar} --add-exports=org.graalvm.truffle.compiler/com.oracle.truffle.compiler=jdk.graal.compiler --add-exports=org.graalvm.truffle.compiler/com.oracle.truffle.compiler.hotspot=jdk.graal.compiler --add-exports=org.graalvm.truffle.compiler/com.oracle.truffle.compiler.hotspot.libgraal=jdk.graal.compiler --add-exports=org.graalvm.word/org.graalvm.word.impl=jdk.graal.compiler + + + + + diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java b/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java index fc257e2c..bfb20d00 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java @@ -27,7 +27,6 @@ import javax.script.Bindings; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import org.slf4j.Logger; @@ -38,6 +37,7 @@ import com.github.jknack.handlebars.internal.Files; import com.github.jknack.handlebars.internal.FormatterChain; import com.github.jknack.handlebars.internal.HbsParserFactory; +import com.github.jknack.handlebars.internal.ScriptEngineFactory; import com.github.jknack.handlebars.internal.Throwing; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import com.github.jknack.handlebars.io.CompositeTemplateLoader; @@ -1442,19 +1442,21 @@ private static EscapingStrategy newEscapeChain(final EscapingStrategy[] chain) { } /** - * @return Nashorn engine. + * @return A JavaScript engine (GraalJS or Nashorn, whichever is available). */ private ScriptEngine engine() { synchronized (this) { if (this.engine == null) { - this.engine = new ScriptEngineManager().getEngineByName("nashorn"); + this.engine = ScriptEngineFactory.create(); - Throwing.run(() -> { - //polyfill globalThis as it is used in handlebars 4.7.9 and is not supported by nashorn - engine.eval("var globalThis = this;"); - engine.eval(Files.read(this.handlebarsJsFile, charset)); - }); + Throwing.run( + () -> { + if (ScriptEngineFactory.isNashorn(engine)) { + engine.eval("var globalThis = this;"); + } + engine.eval(Files.read(this.handlebarsJsFile, charset)); + }); } return this.engine; } diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java b/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java index e8088787..66480d9d 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java @@ -27,7 +27,6 @@ import java.util.regex.Pattern; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +36,7 @@ import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.HelperRegistry; import com.github.jknack.handlebars.internal.Files; +import com.github.jknack.handlebars.internal.ScriptEngineFactory; import com.github.jknack.handlebars.internal.Throwing; /** @@ -175,7 +175,9 @@ public HelperRegistry registerHelpers(final String filename, final String source notNull(filename, "The filename is required."); notEmpty(source, "The source is required."); ScriptEngine engine = engine(); - Throwing.run(() -> engine.eval(adaptES6Literals(source))); + String adaptedSource = + ScriptEngineFactory.isNashorn(engine) ? adaptES6Literals(source) : source; + Throwing.run(() -> engine.eval(adaptedSource)); return this; } @@ -276,12 +278,12 @@ public DefaultHelperRegistry setCharset(final Charset charset) { } /** - * @return Nashorn engine. + * @return A JavaScript engine (GraalJS or Nashorn, whichever is available). */ private ScriptEngine engine() { synchronized (this) { if (this.engine == null) { - this.engine = new ScriptEngineManager().getEngineByName("nashorn"); + this.engine = ScriptEngineFactory.create(); this.engine.put("Handlebars_java", this); diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java new file mode 100644 index 00000000..5d37ecdb --- /dev/null +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java @@ -0,0 +1,101 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.github.jknack.handlebars.internal; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +/** + * Factory for creating a JavaScript {@link ScriptEngine}. By default tries Nashorn first, then + * falls back to GraalJS. The engine can be forced via the {@code hbs.js_engine} system property. + */ +public final class ScriptEngineFactory { + + private ScriptEngineFactory() {} + + /** + * Create a JavaScript engine. If the system property {@code hbs.js_engine} is set, only that + * engine is tried. Otherwise tries Nashorn first, then GraalJS. + * + * @return a JavaScript ScriptEngine. + * @throws IllegalStateException if no JavaScript engine is available or the requested engine is + * not found. + */ + public static ScriptEngine create() { + String requested = System.getProperty("hbs.js_engine"); + if (requested != null) { + return createRequested(requested); + } + return createDefault(); + } + + private static ScriptEngine createDefault() { + ScriptEngineManager manager = new ScriptEngineManager(); + + ScriptEngine engine = manager.getEngineByName("nashorn"); + if (engine != null) { + return engine; + } + + engine = manager.getEngineByName("graal.js"); + if (engine != null) { + configureGraalJS(engine); + return engine; + } + + throw new IllegalStateException( + "No JavaScript engine found. Add either GraalJS or Nashorn to the classpath."); + } + + private static ScriptEngine createRequested(String name) { + ScriptEngineManager manager = new ScriptEngineManager(); + + switch (name) { + case "nashorn": + ScriptEngine nashorn = manager.getEngineByName("nashorn"); + if (nashorn == null) { + throw new IllegalStateException( + "JavaScript engine 'nashorn' requested via hbs.js_engine but is not available." + + " Add org.openjdk.nashorn:nashorn-core to the classpath."); + } + return nashorn; + + case "graaljs": + ScriptEngine graaljs = manager.getEngineByName("graal.js"); + if (graaljs == null) { + throw new IllegalStateException( + "JavaScript engine 'graaljs' requested via hbs.js_engine but is not available." + + " Add org.graalvm.js:js-scriptengine to the classpath."); + } + configureGraalJS(graaljs); + return graaljs; + + default: + throw new IllegalStateException( + "Unknown hbs.js_engine value: '" + + name + + "'. Supported values are 'nashorn' and 'graaljs'."); + } + } + + private static void configureGraalJS(ScriptEngine engine) { + Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); + bindings.put("polyglot.js.allowAllAccess", true); + bindings.put("polyglot.js.nashorn-compat", true); + bindings.put("polyglot.js.ecmascript-version", "2022"); + } + + /** + * Check whether the given engine is Nashorn. + * + * @param engine the script engine. + * @return true if the engine is Nashorn. + */ + public static boolean isNashorn(ScriptEngine engine) { + return engine.getFactory().getEngineName().toLowerCase().contains("nashorn"); + } +} diff --git a/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java b/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java index 218db605..2d9b8cc8 100644 --- a/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java +++ b/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java @@ -13,11 +13,11 @@ import java.util.Map; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import javax.script.SimpleBindings; import org.junit.jupiter.api.Test; +import com.github.jknack.handlebars.internal.ScriptEngineFactory; import com.github.jknack.handlebars.js.JavaScriptHelperTest; public class NashornTest { @@ -45,13 +45,13 @@ public String getName() { @Test public void bootstrap() throws Exception { Handlebars hbs = new Handlebars(); - ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn"); + ScriptEngine engine = ScriptEngineFactory.create(); SimpleBindings bindings = new SimpleBindings(); bindings.put("Handlebars_java", hbs); - nashorn.eval( + engine.eval( new FileReader(Paths.get("src/main/resources/helpers.nashorn.js").toFile()), bindings); - nashorn.eval( + engine.eval( new FileReader( Paths.get("src/test/resources/com/github/jknack/handlebars/js/helpers.js").toFile()), bindings);