You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/08/03 06:56:32 UTC

(hop) branch main updated: issue #2618 : allow selecting ECMAScript language level in JavaScript transform (#7733)

This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new fc022c6009 issue #2618 : allow selecting ECMAScript language level in JavaScript transform (#7733)
fc022c6009 is described below

commit fc022c60093ac2ea500b8bcf19f36dbcf7a74441
Author: Matt Casters <ma...@gmail.com>
AuthorDate: Mon Aug 3 08:56:26 2026 +0200

    issue #2618 : allow selecting ECMAScript language level in JavaScript transform (#7733)
    
    Expose Rhino language levels (DEFAULT, 1.5-1.8, ES6, ECMASCRIPT) in the
    transform UI and metadata so users can use modern syntax such as let/const
    while keeping older scripts on legacy levels. Default remains ES6 for
    compatibility with existing pipelines.
---
 .../ROOT/pages/pipeline/transforms/javascript.adoc |  47 +++++-
 .../transforms/javascript/ScriptValues.java        |  16 ++-
 .../transforms/javascript/ScriptValuesDialog.java  |  53 ++++++-
 .../javascript/ScriptValuesEcmaVersion.java        | 158 +++++++++++++++++++++
 .../transforms/javascript/ScriptValuesMeta.java    |  21 +++
 .../javascript/messages/messages_en_US.properties  |  12 ++
 .../javascript/ScriptValuesEcmaVersionTest.java    | 101 +++++++++++++
 .../javascript/ScriptValuesMetaTest.java           |   1 +
 .../javascript/src/test/resources/javascript.xml   |   1 +
 9 files changed, 407 insertions(+), 3 deletions(-)

diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc
index 738d3d8c30..30a9cfd6d5 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc
@@ -127,9 +127,54 @@ The Javascript pane is the editing area for writing your code. You can insert co
 
 The position at the bottom of the Javascript panel displays the line number and position of the cursor.
 
+The `ECMAScript level` selects the Mozilla Rhino language level used to compile and run your scripts. The default is *ES6*, which matches the engine default in recent Rhino releases and supports modern syntax such as `let`, `const`, arrow functions, and template literals.
+
+Existing pipelines that do not store a language level continue to run at *ES6*, so current behavior is preserved. Choose a legacy level only when a script depends on older JavaScript semantics.
+
+NOTE: Support is limited to what the embedded Rhino engine implements. Some modern features (for example `class` syntax or `async`/`await`) may still be unavailable even at ES6 or ECMASCRIPT.
+
+=== A short history of the language levels
+
+JavaScript grew up in the browser under Netscape and later Mozilla.
+Before the language was primarily described by yearly *ECMAScript* editions (ES5, ES6/ES2015, ES2016, …), Mozilla shipped numbered *JavaScript* releases (1.5, 1.6, 1.7, 1.8) in SpiderMonkey—the engine that powered Firefox.
+
+*Rhino* is Mozilla’s Java implementation of that same language family.
+It was built so server-side and embedded applications (like Hop pipelines) could run JavaScript on the JVM with tight Java interoperability.
+Rhino kept those historic language version switches so embedders could opt into newer syntax without silently breaking older scripts.
+Hop exposes that choice as *ECMAScript level*.
+
+Rough timeline:
+
+* **JavaScript 1.0–1.4** — Early Netscape era (`var`, functions, prototypes, basic objects). Hop lists levels starting at the more complete **1.5** baseline.
+* **JavaScript 1.5** (around 2000, Firefox 1.0 era) — Aligns with *ECMAScript 3*: exception-based error handling, richer regular expressions, `Number` formatting helpers (`toFixed`, `toPrecision`, …), getters/setters, and other ES3-era language work. This is the classic “pre-modern” scripting baseline many long-lived scripts still assume.
+* **JavaScript 1.6** (around 2005, Firefox 1.5) — Functional-style *Array* helpers: `indexOf`, `lastIndexOf`, `every`, `filter`, `forEach`, `map`, `some`, plus Array/String generics. Also introduced *E4X* (ECMAScript for XML) and related XML-oriented syntax that some older Hop samples still demonstrate.
+* **JavaScript 1.7** (around 2006, Firefox 2) — Block scoping with `let`, iterators and generators (`yield`), early `for…of`-style iteration ideas, and destructuring assignment. Several ideas here later became standard ECMAScript; a few Mozilla-only experiments (such as array comprehensions) did not.
+* **JavaScript 1.8** (around 2008, Firefox 3) — Array reduction (`reduce`, `reduceRight`) and more generator/expression-oriented features. Still the last of the old “JavaScript 1.x” marketing line before the industry moved fully to ES5/ES6 versioning.
+* **ES5** (2009) — Strict mode, `JSON`, `Object.create` / property descriptors, and other library/language cleanups. Rhino’s numbered 1.x levels predate calling this out as its own Hop combo entry; much of ES5 is available when you move to modern levels.
+* **ES6 / ES2015 and later** — The big modern leap: proper `let`/`const` semantics, arrow functions, template literals, classes (where implemented), promises, modules, and a long tail of yearly ECMAScript features. Rhino models this mainly as *VERSION_ES6* and, for the newest engine behavior, *VERSION_ECMASCRIPT*.
+
+Why offer the old levels at all? Because language upgrades are not always additive in practice.
+A script written for a looser 1.5-era engine can break under ES6 rules (reserved words, stricter scoping, different edge-case semantics).
+Keeping **DEFAULT** and **1.5–1.8** available means you can modernize new transforms with **ES6** while leaving battle-tested legacy scripts on the level they were written for.
+
+=== Available levels
+
+[%header]
+|===
+|Level|When it appeared (approx.)|Notable capabilities
+|DEFAULT|Rhino legacy default|Odd historical “default” mode kept for compatibility. Limited modern syntax (for example `let` is not available). Prefer an explicit 1.x or ES6 level for new work.
+|1.5|~2000 (ES3 / JS 1.5)|`var`-centric scripts, ES3 exceptions and regex features, classic `Number` formatting methods, getters/setters.
+|1.6|~2005|Array extras (`map`, `filter`, `forEach`, `some`, `every`, `indexOf`, …), Array/String generics, E4X XML features where enabled.
+|1.7|~2006|`let`, generators/`yield`, iterator-style looping, destructuring assignment (Mozilla-era forms).
+|1.8|~2008|`Array.reduce` / `reduceRight`, expression closures and further generator-oriented features from that era.
+|ES6 (default)|ES2015+ (engine default since Rhino 1.8.0)|Modern syntax Rhino implements: `let`/`const`, arrow functions, template literals, spread, and many later library features. Default for new transforms and for pipelines that omit the setting.
+|ECMASCRIPT|Ongoing|Latest ECMAScript level supported by the embedded Rhino build. Tracks the engine’s newest work and may enable stricter checks than ES6.
+|===
+
+
 The `Optimization level` selects the level of JavaScript optimization. The values are:
 
-* 1: JavaScript runs in interpreted mode.
+* -1: JavaScript runs in interpreted mode.
 * 0: No optimizations are performed.
 * 1-9: All optimizations are performed. 9 performs the most optimization with faster script execution, but compiles slower. The default is 9.
 
diff --git a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
index 8588ceba96..c9674a3204 100644
--- a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
+++ b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
@@ -183,9 +183,23 @@ public class ScriptValues extends BaseTransform<ScriptValuesMeta, ScriptValuesDa
         }
       }
 
-      // set the optimization level
+      // set language version and optimization level
       data.context = ContextFactory.getGlobal().enterContext();
 
+      try {
+        String languageVersionAsString = resolve(meta.getLanguageVersion());
+        ScriptValuesEcmaVersion languageVersion =
+            ScriptValuesEcmaVersion.fromCode(languageVersionAsString);
+        languageVersion.applyTo(data.context);
+        if (isBasic()) {
+          logBasic(
+              BaseMessages.getString(
+                  PKG, "ScriptValuesMod.LanguageVersion.Level", languageVersion.getCode()));
+        }
+      } catch (HopException e) {
+        throw new HopTransformException(e.getMessage(), e);
+      }
+
       try {
         String optimizationLevelAsString = resolve(meta.getOptimizationLevel());
         if (!Utils.isEmpty(Const.trim(optimizationLevelAsString))) {
diff --git a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
index 7106a3c557..9da945a3e6 100644
--- a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
+++ b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
@@ -50,6 +50,7 @@ import org.apache.hop.ui.core.dialog.MessageDialogWithToggle;
 import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.HopTree;
 import org.apache.hop.ui.core.widget.JavaScriptStyledTextComp;
 import org.apache.hop.ui.core.widget.StyledTextComp;
@@ -162,6 +163,7 @@ public class ScriptValuesDialog extends BaseTransformDialog {
 
   private final ScriptValuesMeta input;
   private ScriptValuesHelp scVHelp;
+  private ComboVar wLanguageVersion;
   private TextVar wOptimizationLevel;
 
   private TreeItem iteminput;
@@ -292,6 +294,30 @@ public class ScriptValuesDialog extends BaseTransformDialog {
     wOptimizationLevel.setLayoutData(fdOptimizationLevel);
     wOptimizationLevel.addModifyListener(lsMod);
 
+    // ECMAScript language level (Rhino)
+    //
+    Label wlLanguageVersion = new Label(wTop, SWT.NONE);
+    wlLanguageVersion.setText(
+        BaseMessages.getString(PKG, "ScriptValuesDialogMod.LanguageVersion.Label"));
+    PropsUi.setLook(wlLanguageVersion);
+    FormData fdlLanguageVersion = new FormData();
+    fdlLanguageVersion.left = new FormAttachment(wTree, margin);
+    fdlLanguageVersion.bottom = new FormAttachment(wOptimizationLevel, -margin);
+    wlLanguageVersion.setLayoutData(fdlLanguageVersion);
+
+    wLanguageVersion = new ComboVar(variables, wTop, SWT.BORDER | SWT.READ_ONLY);
+    wLanguageVersion.setEditable(true);
+    wLanguageVersion.setItems(ScriptValuesEcmaVersion.getDescriptions());
+    wLanguageVersion.setToolTipText(
+        BaseMessages.getString(PKG, "ScriptValuesDialogMod.LanguageVersion.Tooltip"));
+    PropsUi.setLook(wLanguageVersion);
+    FormData fdLanguageVersion = new FormData();
+    fdLanguageVersion.left = new FormAttachment(wlLanguageVersion, margin);
+    fdLanguageVersion.top = new FormAttachment(wlLanguageVersion, 0, SWT.CENTER);
+    fdLanguageVersion.right = new FormAttachment(100, margin);
+    wLanguageVersion.setLayoutData(fdLanguageVersion);
+    wLanguageVersion.addModifyListener(lsMod);
+
     // The position just above that and below the script...
     //
     wlPosition = new Label(wTop, SWT.LEFT);
@@ -300,7 +326,7 @@ public class ScriptValuesDialog extends BaseTransformDialog {
     FormData fdlPosition = new FormData();
     fdlPosition.left = new FormAttachment(wTree, margin);
     fdlPosition.right = new FormAttachment(100, 0);
-    fdlPosition.bottom = new FormAttachment(wOptimizationLevel, -margin);
+    fdlPosition.bottom = new FormAttachment(wLanguageVersion, -margin);
     wlPosition.setLayoutData(fdlPosition);
 
     folder = new CTabFolder(wTop, SWT.BORDER | SWT.RESIZE);
@@ -764,6 +790,19 @@ public class ScriptValuesDialog extends BaseTransformDialog {
       wOptimizationLevel.setText(ScriptValuesMeta.OPTIMIZATION_LEVEL_DEFAULT);
     }
 
+    try {
+      ScriptValuesEcmaVersion languageVersion =
+          ScriptValuesEcmaVersion.fromCode(input.getLanguageVersion());
+      wLanguageVersion.setText(languageVersion.getDescription());
+    } catch (HopException e) {
+      // Keep the raw value so the user can correct it (or use a variable)
+      if (!Utils.isEmpty(input.getLanguageVersion())) {
+        wLanguageVersion.setText(input.getLanguageVersion().trim());
+      } else {
+        wLanguageVersion.setText(ScriptValuesEcmaVersion.ES6.getDescription());
+      }
+    }
+
     for (int i = 0; i < input.getScriptFields().size(); i++) {
       ScriptValuesMeta.ScriptField field = input.getScriptFields().get(i);
       if (!Utils.isEmpty(field.getName())) {
@@ -866,6 +905,8 @@ public class ScriptValuesDialog extends BaseTransformDialog {
 
   private void getInfo(ScriptValuesMeta meta) {
     meta.setOptimizationLevel(wOptimizationLevel.getText());
+    meta.setLanguageVersion(
+        ScriptValuesEcmaVersion.codeFromDescription(wLanguageVersion.getText()));
 
     meta.getScriptFields().clear();
     for (TableItem item : wFields.getNonEmptyItems()) {
@@ -1146,6 +1187,15 @@ public class ScriptValuesDialog extends BaseTransformDialog {
 
     jscx = ContextFactory.getGlobal().enterContext();
     jscx.setOptimizationLevel(-1);
+    try {
+      ScriptValuesEcmaVersion.apply(
+          jscx,
+          variables.resolve(
+              ScriptValuesEcmaVersion.codeFromDescription(wLanguageVersion.getText())));
+    } catch (HopException e) {
+      testException = e;
+      retval = false;
+    }
     jsscope = jscx.initStandardObjects(null, false);
 
     // Adding the existing Scripts to the Context
@@ -1851,6 +1901,7 @@ public class ScriptValuesDialog extends BaseTransformDialog {
       int lineno,
       Object securityDomain) {
     CompilerEnvirons evn = new CompilerEnvirons();
+    evn.initFromContext(cx);
     evn.setOptimizationLevel(-1);
     evn.setGeneratingSource(true);
     evn.setGenerateDebugInfo(true);
diff --git a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesEcmaVersion.java b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesEcmaVersion.java
new file mode 100644
index 0000000000..823da6a6de
--- /dev/null
+++ b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesEcmaVersion.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.javascript;
+
+import java.util.Arrays;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.i18n.BaseMessages;
+import org.mozilla.javascript.Context;
+
+/**
+ * Supported Mozilla Rhino ECMAScript / JavaScript language levels for the JavaScript transform.
+ *
+ * <p>Stable codes are stored in pipeline XML. The engine default used by recent Rhino releases (and
+ * by this transform when the field is empty) is {@link #ES6}.
+ */
+public enum ScriptValuesEcmaVersion {
+  /** Legacy Rhino default; limited modern syntax (for example no {@code let}). */
+  DEFAULT("DEFAULT", Context.VERSION_DEFAULT),
+  JS_1_5("1.5", Context.VERSION_1_5),
+  JS_1_6("1.6", Context.VERSION_1_6),
+  JS_1_7("1.7", Context.VERSION_1_7),
+  JS_1_8("1.8", Context.VERSION_1_8),
+  /**
+   * ECMAScript 6 and later features implemented by Rhino. Default for new transforms and for Rhino
+   * itself since 1.8.
+   */
+  ES6("ES6", Context.VERSION_ES6),
+  /**
+   * Latest ECMAScript level supported by the embedded Rhino engine (may enable stricter checks than
+   * {@link #ES6}).
+   */
+  ECMASCRIPT("ECMASCRIPT", Context.VERSION_ECMASCRIPT);
+
+  private static final Class<?> PKG = ScriptValuesMeta.class;
+
+  /** Stable code stored in metadata / XML for new transforms (matches Rhino's default). */
+  public static final String DEFAULT_CODE = ES6.code;
+
+  private final String code;
+  private final int rhinoVersion;
+
+  ScriptValuesEcmaVersion(String code, int rhinoVersion) {
+    this.code = code;
+    this.rhinoVersion = rhinoVersion;
+  }
+
+  public String getCode() {
+    return code;
+  }
+
+  public int getRhinoVersion() {
+    return rhinoVersion;
+  }
+
+  /** Localized UI label for this level. */
+  public String getDescription() {
+    return BaseMessages.getString(PKG, "ScriptValuesMod.LanguageVersion." + code);
+  }
+
+  /** All localized descriptions in enum order (for combo boxes). */
+  public static String[] getDescriptions() {
+    return Arrays.stream(values())
+        .map(ScriptValuesEcmaVersion::getDescription)
+        .toArray(String[]::new);
+  }
+
+  /** Codes only, for tests and non-UI lists. */
+  public static String[] getCodes() {
+    return Arrays.stream(values()).map(v -> v.code).toArray(String[]::new);
+  }
+
+  /**
+   * Resolve a stored code, description, or numeric Rhino version to a known level. Empty input maps
+   * to {@link #ES6} (engine default / compatible with existing pipelines on recent Rhino).
+   */
+  public static ScriptValuesEcmaVersion fromCode(String value) throws HopException {
+    if (Utils.isEmpty(value) || Utils.isEmpty(value.trim())) {
+      return ES6;
+    }
+    String trimmed = value.trim();
+
+    for (ScriptValuesEcmaVersion version : values()) {
+      if (version.code.equalsIgnoreCase(trimmed)) {
+        return version;
+      }
+    }
+
+    for (ScriptValuesEcmaVersion version : values()) {
+      if (version.getDescription().equals(trimmed)) {
+        return version;
+      }
+    }
+
+    // Numeric Rhino language version (e.g. 200, 250)
+    try {
+      int numeric = Integer.parseInt(trimmed);
+      for (ScriptValuesEcmaVersion version : values()) {
+        if (version.rhinoVersion == numeric) {
+          return version;
+        }
+      }
+    } catch (NumberFormatException ignored) {
+      // fall through to error
+    }
+
+    throw new HopException(
+        BaseMessages.getString(
+            PKG, "ScriptValuesMetaMod.Exception.UnsupportedLanguageVersion", trimmed));
+  }
+
+  /**
+   * Map a UI description back to the stable storage code. Falls back to the text itself when it is
+   * already a code or a variable expression.
+   */
+  public static String codeFromDescription(String description) {
+    if (Utils.isEmpty(description)) {
+      return DEFAULT_CODE;
+    }
+    String trimmed = description.trim();
+    for (ScriptValuesEcmaVersion version : values()) {
+      if (version.getDescription().equals(trimmed) || version.code.equalsIgnoreCase(trimmed)) {
+        return version.code;
+      }
+    }
+    return trimmed;
+  }
+
+  /** Apply this language level to a Rhino {@link Context}. */
+  public void applyTo(Context context) {
+    context.setLanguageVersion(rhinoVersion);
+  }
+
+  /**
+   * Resolve {@code languageVersion} (code or description) and set it on the context.
+   *
+   * @param context Rhino context
+   * @param languageVersion language version code/description (already variable-resolved)
+   */
+  public static void apply(Context context, String languageVersion) throws HopException {
+    fromCode(languageVersion).applyTo(context);
+  }
+}
diff --git a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
index 2b0ee74ef6..5618b4f50e 100644
--- a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
+++ b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
@@ -63,6 +63,9 @@ public class ScriptValuesMeta extends BaseTransformMeta<ScriptValues, ScriptValu
 
   public static final String OPTIMIZATION_LEVEL_DEFAULT = "9";
 
+  /** Default ECMAScript language level code ({@link ScriptValuesEcmaVersion#DEFAULT_CODE}). */
+  public static final String LANGUAGE_VERSION_DEFAULT = ScriptValuesEcmaVersion.DEFAULT_CODE;
+
   @Getter
   @Setter
   public static class ScriptField {
@@ -135,11 +138,22 @@ public class ScriptValuesMeta extends BaseTransformMeta<ScriptValues, ScriptValu
       injectionKeyDescription = "ScriptValuesMod.Injection.OPTIMIZATION_LEVEL")
   private String optimizationLevel;
 
+  /**
+   * Rhino ECMAScript language level code (see {@link ScriptValuesEcmaVersion}). Empty or missing
+   * values use {@link #LANGUAGE_VERSION_DEFAULT} (ES6), matching Rhino's current engine default.
+   */
+  @HopMetadataProperty(
+      key = "languageVersion",
+      injectionKey = "LANGUAGE_VERSION",
+      injectionKeyDescription = "ScriptValuesMod.Injection.LANGUAGE_VERSION")
+  private String languageVersion;
+
   public ScriptValuesMeta() {
     super();
     jsScripts = new ArrayList<>();
     scriptFields = new ArrayList<>();
     optimizationLevel = OPTIMIZATION_LEVEL_DEFAULT;
+    languageVersion = LANGUAGE_VERSION_DEFAULT;
 
     ScriptValuesScript script = new ScriptValuesScript();
     script.setType(ScriptValuesScript.TRANSFORM_SCRIPT);
@@ -152,6 +166,7 @@ public class ScriptValuesMeta extends BaseTransformMeta<ScriptValues, ScriptValu
   public ScriptValuesMeta(ScriptValuesMeta m) {
     this();
     this.optimizationLevel = m.optimizationLevel;
+    this.languageVersion = m.languageVersion;
     m.jsScripts.forEach(s -> this.jsScripts.add(new ScriptValuesScript(s)));
     m.scriptFields.forEach(f -> scriptFields.add(new ScriptField(f)));
   }
@@ -248,6 +263,12 @@ public class ScriptValuesMeta extends BaseTransformMeta<ScriptValues, ScriptValu
 
     jsContext = ContextFactory.getGlobal().enterContext();
     jsScope = jsContext.initStandardObjects(null, false);
+    try {
+      ScriptValuesEcmaVersion.apply(jsContext, variables.resolve(languageVersion));
+    } catch (HopException e) {
+      cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, e.getMessage(), transformMeta);
+      remarks.add(cr);
+    }
     try {
       jsContext.setOptimizationLevel(Integer.parseInt(variables.resolve(optimizationLevel)));
     } catch (NumberFormatException nfe) {
diff --git a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
index c1c06d5c3f..80984b56f7 100644
--- a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
+++ b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
@@ -46,6 +46,8 @@ ScriptValuesDialogMod.JavascriptFunctions.Label=Java script functions \:
 ScriptValuesDialogMod.LogicFunctions.Label=Logic Functions
 ScriptValuesDialogMod.NoActiveScriptSet=No active Script has been set\! Should the first tab set as active Script?
 ScriptValuesDialogMod.NumericFunctions.Label=Numeric Functions
+ScriptValuesDialogMod.LanguageVersion.Label=ECMAScript level
+ScriptValuesDialogMod.LanguageVersion.Tooltip=Rhino JavaScript language level. ES6 (default) supports let, const, arrow functions, template literals and more. Choose a legacy level (DEFAULT, 1.5-1.8) for older scripts that rely on pre-ES6 behavior. ECMASCRIPT tracks the latest features supported by the embedded Rhino engine.
 ScriptValuesDialogMod.OptimizationLevel.Label=Optimization level
 ScriptValuesDialogMod.OptimizationLevel.Tooltip=Valid values are -1 for interpretive mode, 0 for no optimizations, and a value from 1 - 9 where 9 specifies maximal optimization
 ScriptValuesDialogMod.OutputFields.Label=Output fields
@@ -82,6 +84,7 @@ ScriptValuesMetaMod.CheckResult.ScriptCompiledOK=Script compiled without a probl
 ScriptValuesMetaMod.CheckResult.ScriptCompiledOK2=Script executed without a problem
 ScriptValuesMetaMod.Exception.FieldToReplaceNotFound=Field to replace [{0}] was not found in the transform input fields list.
 ScriptValuesMetaMod.Exception.NumberFormatException=Optimization level: Could not convert "{0}" to a number.
+ScriptValuesMetaMod.Exception.UnsupportedLanguageVersion=Unsupported ECMAScript language level "{0}". Valid values are DEFAULT, 1.5, 1.6, 1.7, 1.8, ES6, ECMASCRIPT (or the corresponding Rhino version numbers 0, 150, 160, 170, 180, 200, 250).
 ScriptValuesMetaMod.Exception.UnableToLoadAdditionalClass=Unable to load additional classes
 ScriptValuesMetaMod.Exception.UnableToLoadTransformMetaFromXML=Unable to load transform info from XML
 ScriptValuesMetaMod.Exception.UnableToParseXMLforAdditionalClasses=Unable to parse XML for additional classes (future feature)
@@ -94,11 +97,20 @@ ScriptValuesMod.Injection.FIELD_RENAME_TO=The name that the output field will be
 ScriptValuesMod.Injection.FIELD_REPLACE=This setting controls the behavior of adding or replacing fields.
 ScriptValuesMod.Injection.FIELD_TYPE=The field type.
 ScriptValuesMod.Injection.FIELDS=The output fields to write to.
+ScriptValuesMod.Injection.LANGUAGE_VERSION=Rhino ECMAScript language level (DEFAULT, 1.5, 1.6, 1.7, 1.8, ES6, ECMASCRIPT).
 ScriptValuesMod.Injection.OPTIMIZATION_LEVEL=Specify the level of optimization.
 ScriptValuesMod.Injection.SCRIPT=The java script statement.
 ScriptValuesMod.Injection.SCRIPT_TYPE=The type of script: -1=normal, 0=transform, 1=start, 2=end
 ScriptValuesMod.Injection.SCRIPT_NAME=The name of the script.
 ScriptValuesMod.Injection.SCRIPTS=The java scripts.
+ScriptValuesMod.LanguageVersion.DEFAULT=DEFAULT - Legacy Rhino (limited modern syntax)
+ScriptValuesMod.LanguageVersion.1.5=1.5 - JavaScript 1.5
+ScriptValuesMod.LanguageVersion.1.6=1.6 - JavaScript 1.6
+ScriptValuesMod.LanguageVersion.1.7=1.7 - JavaScript 1.7
+ScriptValuesMod.LanguageVersion.1.8=1.8 - JavaScript 1.8
+ScriptValuesMod.LanguageVersion.ES6=ES6 - ECMAScript 6+ (let, const, arrows, ...) [default]
+ScriptValuesMod.LanguageVersion.ECMASCRIPT=ECMASCRIPT - Latest supported by Rhino
+ScriptValuesMod.LanguageVersion.Level=ECMAScript language level set to {0}.
 ScriptValuesMod.Log.CouldNotAddDefaultConstants=Could not add default constants to the project.
 ScriptValuesMod.Log.CouldNotAddDefaultFunctions=Could not add default functions to the project.
 ScriptValuesMod.Log.CouldNotAttachAdditionalScripts=Could not attach additional scripts to the project.
diff --git a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesEcmaVersionTest.java b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesEcmaVersionTest.java
new file mode 100644
index 0000000000..b0090b0820
--- /dev/null
+++ b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesEcmaVersionTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.javascript;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.exception.HopException;
+import org.junit.jupiter.api.Test;
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.ContextFactory;
+import org.mozilla.javascript.Scriptable;
+
+class ScriptValuesEcmaVersionTest {
+
+  @Test
+  void emptyDefaultsToEs6() throws Exception {
+    assertEquals(ScriptValuesEcmaVersion.ES6, ScriptValuesEcmaVersion.fromCode(null));
+    assertEquals(ScriptValuesEcmaVersion.ES6, ScriptValuesEcmaVersion.fromCode(""));
+    assertEquals(ScriptValuesEcmaVersion.ES6, ScriptValuesEcmaVersion.fromCode("  "));
+  }
+
+  @Test
+  void resolvesCodesCaseInsensitively() throws Exception {
+    assertEquals(ScriptValuesEcmaVersion.ES6, ScriptValuesEcmaVersion.fromCode("es6"));
+    assertEquals(
+        ScriptValuesEcmaVersion.ECMASCRIPT, ScriptValuesEcmaVersion.fromCode("ECMASCRIPT"));
+    assertEquals(ScriptValuesEcmaVersion.DEFAULT, ScriptValuesEcmaVersion.fromCode("DEFAULT"));
+    assertEquals(ScriptValuesEcmaVersion.JS_1_7, ScriptValuesEcmaVersion.fromCode("1.7"));
+  }
+
+  @Test
+  void resolvesNumericRhinoVersions() throws Exception {
+    assertEquals(ScriptValuesEcmaVersion.ES6, ScriptValuesEcmaVersion.fromCode("200"));
+    assertEquals(ScriptValuesEcmaVersion.ECMASCRIPT, ScriptValuesEcmaVersion.fromCode("250"));
+    assertEquals(ScriptValuesEcmaVersion.DEFAULT, ScriptValuesEcmaVersion.fromCode("0"));
+  }
+
+  @Test
+  void rejectsUnknownValues() {
+    assertThrows(HopException.class, () -> ScriptValuesEcmaVersion.fromCode("ES2024"));
+    assertThrows(HopException.class, () -> ScriptValuesEcmaVersion.fromCode("999"));
+  }
+
+  @Test
+  void codeFromDescriptionRoundTrips() {
+    for (ScriptValuesEcmaVersion version : ScriptValuesEcmaVersion.values()) {
+      assertEquals(
+          version.getCode(), ScriptValuesEcmaVersion.codeFromDescription(version.getCode()));
+    }
+  }
+
+  @Test
+  void es6AllowsLetAndConst() throws Exception {
+    Context cx = ContextFactory.getGlobal().enterContext();
+    try {
+      ScriptValuesEcmaVersion.ES6.applyTo(cx);
+      Scriptable scope = cx.initStandardObjects();
+      Object result = cx.evaluateString(scope, "let a = 1; const b = 2; a + b", "t", 1, null);
+      assertEquals(3.0, Context.toNumber(result), 0.0001);
+    } finally {
+      Context.exit();
+    }
+  }
+
+  @Test
+  void legacyDefaultRejectsLet() throws Exception {
+    Context cx = ContextFactory.getGlobal().enterContext();
+    try {
+      ScriptValuesEcmaVersion.DEFAULT.applyTo(cx);
+      Scriptable scope = cx.initStandardObjects();
+      assertThrows(Exception.class, () -> cx.evaluateString(scope, "let a = 1; a", "t", 1, null));
+    } finally {
+      Context.exit();
+    }
+  }
+
+  @Test
+  void descriptionsAreNonEmpty() {
+    String[] descriptions = ScriptValuesEcmaVersion.getDescriptions();
+    assertEquals(ScriptValuesEcmaVersion.values().length, descriptions.length);
+    for (String description : descriptions) {
+      assertTrue(description != null && !description.isBlank());
+    }
+  }
+}
diff --git a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
index 018d10a9ec..57a8c78090 100644
--- a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
+++ b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
@@ -54,6 +54,7 @@ class ScriptValuesMetaTest {
     // See if after all that we still have the correct data
     //
     assertEquals("2", meta.getOptimizationLevel());
+    assertEquals("ES6", meta.getLanguageVersion());
 
     // The scripts
     assertEquals(3, meta.getJsScripts().size());
diff --git a/plugins/transforms/javascript/src/test/resources/javascript.xml b/plugins/transforms/javascript/src/test/resources/javascript.xml
index 964c6d5e31..2b0fe35b2f 100644
--- a/plugins/transforms/javascript/src/test/resources/javascript.xml
+++ b/plugins/transforms/javascript/src/test/resources/javascript.xml
@@ -19,6 +19,7 @@
 
 <transform>
     <optimizationLevel>2</optimizationLevel>
+    <languageVersion>ES6</languageVersion>
     <jsScripts>
         <jsScript>
             <jsScript_type>1</jsScript_type>