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 04ed14caf2 add javascript to the script transform, fixes #6823 (#6835)
04ed14caf2 is described below

commit 04ed14caf250a121caf642c28ec778a713d0218f
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Mar 19 14:20:52 2026 +0100

    add javascript to the script transform, fixes #6823 (#6835)
---
 .../ROOT/pages/pipeline/transforms/script.adoc     |  10 +-
 plugins/transforms/script/pom.xml                  |  13 +
 .../transforms/script/src/assembly/assembly.xml    |   6 +
 .../hop/pipeline/transforms/script/Script.java     |  20 +
 .../pipeline/transforms/script/ScriptUtils.java    |  36 +-
 .../script/ScriptAddedFunctionsTest.java           |  48 +++
 .../pipeline/transforms/script/ScriptDataTest.java |  38 ++
 .../pipeline/transforms/script/ScriptMetaTest.java | 240 ++++++++++++
 .../hop/pipeline/transforms/script/ScriptTest.java | 434 +++++++++++++++++++++
 .../transforms/script/ScriptUtilsTest.java         | 123 ++++++
 .../hop/ui/core/widget/highlight/ScriptEngine.java |  90 +++++
 11 files changed, 1046 insertions(+), 12 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
index fd9c09a934..ed72273540 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
@@ -29,10 +29,11 @@ The Script transform allows you to write code in any 
language supported by the h
 
 By default, the Hop project ships with the following language support:
 
+* https://developer.mozilla.org/en-US/docs/Web/JavaScript[ECMAScript] 
(JavaScript via https://www.graalvm.org/javascript/[GraalVM JavaScript], JSR 
223)
 * https://groovy-lang.org/[Groovy]
 * https://www.python.org/[Python] (https://www.jython.org/[Jython])
 
-WARNING: ECMAScript (JavaScript as part of the JVM) was removed from the JVM 
with Java 17. You can still add it in Apache Hop 2.10 and later as documented 
in the <<_adding_scripting_languages, Adding Scripting Languages>> section.
+On Java 17 and later, the JVM no longer includes an ECMAScript engine; Hop 
bundles GraalVM JavaScript so that the Script transform works without extra 
setup.
 
 
 |
@@ -252,9 +253,4 @@ For example, to add support for the Ruby scripting language 
you need to add the
 * `jruby-stdlib-<version>.jar`
 * `jruby-core-<version>.jar`
 
-To add Javascript support, download the 
https://repo1.maven.org/maven2/org/openjdk/nashorn/nashorn-core/[Nashorn Core 
jar]
-
-* `nashorn-core-15.4.jar`
-
-
-After restarting the Apache Hop GUI you'll notice that there's a `ruby` or 
`Javascript` entry in the `Scripting Engine` dropdown box.
+After restarting the Apache Hop GUI you'll notice that there's a `ruby` entry 
in the `Scripting Engine` dropdown box.
diff --git a/plugins/transforms/script/pom.xml 
b/plugins/transforms/script/pom.xml
index a71dfe6edf..28ad80330b 100644
--- a/plugins/transforms/script/pom.xml
+++ b/plugins/transforms/script/pom.xml
@@ -30,6 +30,7 @@
     <name>Hop Plugins Transforms Script</name>
 
     <properties>
+        <graaljs.version>23.0.2</graaljs.version>
         <groovy.version>4.0.30</groovy.version>
         <jython.version>2.7.4</jython.version>
     </properties>
@@ -57,6 +58,12 @@
             <artifactId>groovy-jsr223</artifactId>
             <version>${groovy.version}</version>
         </dependency>
+        <!-- GraalVM JavaScript (JSR 223) for ECMAScript on Java 17+ where 
Nashorn was removed -->
+        <dependency>
+            <groupId>org.graalvm.js</groupId>
+            <artifactId>js-scriptengine</artifactId>
+            <version>${graaljs.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.ow2.asm</groupId>
             <artifactId>asm</artifactId>
@@ -84,5 +91,11 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.graalvm.js</groupId>
+            <artifactId>js</artifactId>
+            <version>${graaljs.version}</version>
+            <scope>runtime</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/plugins/transforms/script/src/assembly/assembly.xml 
b/plugins/transforms/script/src/assembly/assembly.xml
index 01f59aadc2..424c5bd7e5 100644
--- a/plugins/transforms/script/src/assembly/assembly.xml
+++ b/plugins/transforms/script/src/assembly/assembly.xml
@@ -51,6 +51,12 @@
                 <include>org.apache.groovy:groovy:jar</include>
                 <include>org.apache.groovy:groovy-jsr223:jar</include>
                 <include>org.python:jython-standalone:jar</include>
+                <include>org.graalvm.js:js-scriptengine:jar</include>
+                <include>org.graalvm.js:js:jar</include>
+                <include>org.graalvm.regex:regex:jar</include>
+                <include>org.graalvm.truffle:truffle-api:jar</include>
+                <include>org.graalvm.sdk:graal-sdk:jar</include>
+                <include>com.ibm.icu:icu4j:jar</include>
             </includes>
             <outputDirectory>plugins/transforms/script/lib</outputDirectory>
         </dependencySet>
diff --git 
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
 
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
index 4089523e50..f1d1fa8906 100644
--- 
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
+++ 
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
@@ -459,6 +459,10 @@ public class Script extends BaseTransform<ScriptMeta, 
ScriptData> implements ITr
               }
 
             case IValueMeta.TYPE_INTEGER:
+              // Hop expects Long for TYPE_INTEGER; GraalVM JS can return 
Integer or other Number.
+              if (result instanceof Number) {
+                return Long.valueOf(((Number) result).longValue());
+              }
               if (classType.equalsIgnoreCase("java.lang.Byte")) {
                 return ((Byte) result).longValue();
               } else if (classType.equalsIgnoreCase("java.lang.Short")) {
@@ -674,6 +678,22 @@ public class Script extends BaseTransform<ScriptMeta, 
ScriptData> implements ITr
     return bRC;
   }
 
+  @Override
+  public void putRow(IRowMeta rowMeta, Object[] row) throws 
HopTransformException {
+    // GraalVM JS can produce java.lang.Integer for numeric values; Hop's 
TYPE_INTEGER
+    // expects Long (ValueMetaBase.getInteger does (Long) object). Normalize 
so golden
+    // validation and sorting don't get ClassCastException.
+    if (rowMeta != null && row != null) {
+      for (int i = 0; i < rowMeta.size() && i < row.length; i++) {
+        if (rowMeta.getValueMeta(i).getType() == IValueMeta.TYPE_INTEGER
+            && row[i] instanceof Integer) {
+          row[i] = Long.valueOf(((Integer) row[i]).longValue());
+        }
+      }
+    }
+    super.putRow(rowMeta, row);
+  }
+
   @Override
   public boolean init() {
     if (!super.init()) {
diff --git 
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
 
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
index 095049be98..f709a8bd89 100644
--- 
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
+++ 
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
@@ -18,6 +18,7 @@
 
 package org.apache.hop.pipeline.transforms.script;
 
+import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -26,9 +27,18 @@ import javax.script.ScriptEngine;
 import javax.script.ScriptEngineFactory;
 import javax.script.ScriptEngineManager;
 import org.apache.hop.core.exception.HopException;
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.HostAccess;
 
 public class ScriptUtils {
 
+  static {
+    // Suppress GraalVM "interpreted mode only" warning when running on a 
standard JDK.
+    if (System.getProperty("polyglot.engine.WarnInterpreterOnly") == null) {
+      System.setProperty("polyglot.engine.WarnInterpreterOnly", "false");
+    }
+  }
+
   private static ScriptEngineManager scriptEngineManager;
 
   private static ScriptUtils instance;
@@ -61,8 +71,10 @@ public class ScriptUtils {
    * @return the desired ScriptEngine, or null if none can be found
    */
   private static ScriptEngine createNewScriptEngine(String engineName) {
-
     ScriptEngine scriptEngine = 
getScriptEngineManager().getEngineByName(engineName);
+    if (scriptEngine instanceof GraalJSScriptEngine) {
+      return createGraalJsEngine();
+    }
     if (scriptEngine == null) {
       // falls back to Groovy
       scriptEngine = scriptEngineManager.getEngineByName("groovy");
@@ -105,8 +117,6 @@ public class ScriptUtils {
       System.setProperty(
           "org.jruby.embed.localvariable.behavior",
           "persistent"); // required for JRuby, transparent
-      System.setProperty("nashorn.args", "--language=es6");
-      // for others
       scriptEngineManager = new 
ScriptEngineManager(ScriptUtils.class.getClassLoader());
     }
     return scriptEngineManager;
@@ -136,7 +146,23 @@ public class ScriptUtils {
     }
   }
 
-  public ScriptEngine getScriptEngineByName(String scriptLanguegeName) {
-    return scriptEngineManager.getEngineByName(scriptLanguegeName);
+  public ScriptEngine getScriptEngineByName(String scriptLanguageName) {
+    ScriptEngine engine = 
scriptEngineManager.getEngineByName(scriptLanguageName);
+    // GraalVM JS runs in a restricted sandbox by default when obtained through
+    // ScriptEngineManager. Re-create it with HostAccess.ALL so scripts can 
call
+    // methods on bound Java objects (e.g. transform.logBasic(...)), matching 
the
+    // behaviour of the Nashorn engine that was removed in Java 17.
+    if (engine instanceof GraalJSScriptEngine) {
+      return createGraalJsEngine();
+    }
+    return engine;
+  }
+
+  private static ScriptEngine createGraalJsEngine() {
+    return GraalJSScriptEngine.create(
+        null,
+        Context.newBuilder("js")
+            .allowHostAccess(HostAccess.ALL)
+            .allowHostClassLookup(className -> true));
   }
 }
diff --git 
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptAddedFunctionsTest.java
 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptAddedFunctionsTest.java
new file mode 100644
index 0000000000..46a14831a8
--- /dev/null
+++ 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptAddedFunctionsTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class ScriptAddedFunctionsTest {
+
+  @Test
+  void undefinedValue_isNull() {
+    assertNull(ScriptAddedFunctions.undefinedValue);
+  }
+
+  @Test
+  void functionTypeConstants_haveExpectedValues() {
+    assertEquals(0, ScriptAddedFunctions.STRING_FUNCTION);
+    assertEquals(1, ScriptAddedFunctions.NUMERIC_FUNCTION);
+    assertEquals(2, ScriptAddedFunctions.DATE_FUNCTION);
+    assertEquals(3, ScriptAddedFunctions.LOGIC_FUNCTION);
+    assertEquals(4, ScriptAddedFunctions.SPECIAL_FUNCTION);
+    assertEquals(5, ScriptAddedFunctions.FILE_FUNCTION);
+  }
+
+  @Test
+  void jsFunctionList_isEmptyByDefault() {
+    assertNotNull(ScriptAddedFunctions.jsFunctionList);
+    assertEquals(0, ScriptAddedFunctions.jsFunctionList.length);
+  }
+}
diff --git 
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptDataTest.java
 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptDataTest.java
new file mode 100644
index 0000000000..612dade1aa
--- /dev/null
+++ 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptDataTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.script;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class ScriptDataTest {
+
+  @Test
+  void defaultConstructor_setsNullFields() {
+    ScriptData data = new ScriptData();
+    assertNull(data.engine);
+    assertNull(data.context);
+    assertNull(data.compiledScript);
+    assertNull(data.rawScript);
+    assertNull(data.fieldsUsed);
+    assertNull(data.valuesUsed);
+    assertNull(data.outputRowMeta);
+    assertNull(data.replaceIndex);
+  }
+}
diff --git 
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptMetaTest.java
 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptMetaTest.java
new file mode 100644
index 0000000000..624d1f900e
--- /dev/null
+++ 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptMetaTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class ScriptMetaTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private ScriptMeta meta;
+  private IRowMeta rowMeta;
+  private IVariables variables;
+  private IHopMetadataProvider metadataProvider;
+
+  @BeforeEach
+  void setUp() {
+    meta = new ScriptMeta();
+    rowMeta = new org.apache.hop.core.row.RowMeta();
+    variables = mock(IVariables.class);
+    metadataProvider = mock(IHopMetadataProvider.class);
+  }
+
+  @Test
+  void getLanguageName_defaultIsNull() {
+    assertNull(meta.getLanguageName());
+  }
+
+  @Test
+  void setLanguageName_getLanguageName_roundTrip() {
+    meta.setLanguageName("Groovy");
+    assertEquals("Groovy", meta.getLanguageName());
+    meta.setLanguageName("ECMAScript");
+    assertEquals("ECMAScript", meta.getLanguageName());
+  }
+
+  @Test
+  void getScripts_defaultIsEmptyList() {
+    assertNotNull(meta.getScripts());
+    assertTrue(meta.getScripts().isEmpty());
+  }
+
+  @Test
+  void setScripts_getScripts_roundTrip() {
+    List<ScriptMeta.SScript> scripts = new ArrayList<>();
+    ScriptMeta.SScript s = new ScriptMeta.SScript();
+    s.setScriptName("s1");
+    s.setScript("x=1");
+    scripts.add(s);
+    meta.setScripts(scripts);
+    assertEquals(1, meta.getScripts().size());
+    assertEquals("s1", meta.getScripts().get(0).getScriptName());
+    assertEquals("x=1", meta.getScripts().get(0).getScript());
+  }
+
+  @Test
+  void getFields_defaultIsEmptyList() {
+    assertNotNull(meta.getFields());
+    assertTrue(meta.getFields().isEmpty());
+  }
+
+  @Test
+  void setFields_getFields_roundTrip() {
+    List<ScriptMeta.SField> fields = new ArrayList<>();
+    ScriptMeta.SField f = new ScriptMeta.SField();
+    f.setName("out1");
+    f.setType("String");
+    fields.add(f);
+    meta.setFields(fields);
+    assertEquals(1, meta.getFields().size());
+    assertEquals("out1", meta.getFields().get(0).getName());
+    assertEquals("String", meta.getFields().get(0).getType());
+  }
+
+  @Test
+  void clone_returnsNewInstanceWithSameContent() {
+    meta.setLanguageName("Groovy");
+    meta.getScripts()
+        .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, 
"s1", "1+1"));
+    meta.getFields().add(createField("f1", "Integer"));
+
+    ScriptMeta cloned = meta.clone();
+    assertNotNull(cloned);
+    assertFalse(cloned == meta);
+    assertEquals("Groovy", cloned.getLanguageName());
+    assertEquals(1, cloned.getScripts().size());
+    assertEquals("s1", cloned.getScripts().get(0).getScriptName());
+    assertEquals(1, cloned.getFields().size());
+    assertEquals("f1", cloned.getFields().get(0).getName());
+  }
+
+  private static ScriptMeta.SField createField(String name, String type) {
+    ScriptMeta.SField f = new ScriptMeta.SField();
+    f.setName(name);
+    f.setType(type);
+    return f;
+  }
+
+  @Test
+  void getFields_addsNewFieldWhenNotReplace() throws HopTransformException {
+    ScriptMeta.SField field = createField("newField", "String");
+    field.setRename("newField"); // when rename is set, getFields uses 
getName() for the new field
+    meta.getFields().add(field);
+    meta.getFields(rowMeta, "Script", null, null, variables, metadataProvider);
+    assertEquals(1, rowMeta.size());
+    assertEquals("newField", rowMeta.getValueMeta(0).getName());
+  }
+
+  @Test
+  void getFields_skipsFieldWithEmptyName() throws HopTransformException {
+    meta.getFields().add(createField("", "String"));
+    ScriptMeta.SField valid = createField("valid", "Integer");
+    valid.setRename("valid");
+    meta.getFields().add(valid);
+    meta.getFields(rowMeta, "Script", null, null, variables, metadataProvider);
+    assertEquals(1, rowMeta.size());
+    assertEquals("valid", rowMeta.getValueMeta(0).getName());
+  }
+
+  @Test
+  void getFields_replacesExistingFieldWhenReplaceTrue() throws 
HopTransformException {
+    rowMeta.addValueMeta(new ValueMetaString("existing"));
+    ScriptMeta.SField field = createField("existing", "Integer");
+    field.setReplace(true);
+    field.setRename("existing");
+    meta.getFields().add(field);
+    meta.getFields(rowMeta, "Script", null, null, variables, metadataProvider);
+    assertEquals(1, rowMeta.size());
+    assertEquals("existing", rowMeta.getValueMeta(0).getName());
+    assertEquals(
+        org.apache.hop.core.row.IValueMeta.TYPE_INTEGER, 
rowMeta.getValueMeta(0).getType());
+  }
+
+  @Test
+  void getFields_throwsWhenReplaceTrueAndFieldNotFound() {
+    ScriptMeta.SField field = createField("missing", "String");
+    field.setReplace(true);
+    field.setRename("");
+    meta.getFields().add(field);
+    assertThrows(
+        HopTransformException.class,
+        () -> meta.getFields(rowMeta, "Script", null, null, variables, 
metadataProvider));
+  }
+
+  @Test
+  void sField_getHopType_returnsCorrectType() {
+    ScriptMeta.SField f = new ScriptMeta.SField();
+    f.setType("String");
+    assertEquals(IValueMeta.TYPE_STRING, f.getHopType());
+    f.setType("Integer");
+    assertEquals(IValueMeta.TYPE_INTEGER, f.getHopType());
+  }
+
+  @Test
+  void sField_copyConstructor() {
+    ScriptMeta.SField orig = new ScriptMeta.SField();
+    orig.setName("a");
+    orig.setRename("b");
+    orig.setType("Number");
+    orig.setLength(10);
+    orig.setPrecision(2);
+    orig.setReplace(true);
+    orig.setScriptResult(true);
+    ScriptMeta.SField copy = new ScriptMeta.SField(orig);
+    assertEquals("a", copy.getName());
+    assertEquals("b", copy.getRename());
+    assertEquals("Number", copy.getType());
+    assertEquals(10, copy.getLength());
+    assertEquals(2, copy.getPrecision());
+    assertTrue(copy.isReplace());
+    assertTrue(copy.isScriptResult());
+  }
+
+  @Test
+  void sScript_scriptTypeHelpers() {
+    ScriptMeta.SScript s = new ScriptMeta.SScript();
+    s.setScriptType(ScriptMeta.ScriptType.TRANSFORM_SCRIPT);
+    assertTrue(s.isTransformScript());
+    assertFalse(s.isStartScript());
+    assertFalse(s.isEndScript());
+    s.setScriptType(ScriptMeta.ScriptType.START_SCRIPT);
+    assertTrue(s.isStartScript());
+    s.setScriptType(ScriptMeta.ScriptType.END_SCRIPT);
+    assertTrue(s.isEndScript());
+  }
+
+  @Test
+  void sScript_copyConstructor() {
+    ScriptMeta.SScript orig =
+        new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, "sn", 
"body");
+    ScriptMeta.SScript copy = new ScriptMeta.SScript(orig);
+    assertEquals(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, copy.getScriptType());
+    assertEquals("sn", copy.getScriptName());
+    assertEquals("body", copy.getScript());
+  }
+
+  @Test
+  void scriptType_enumValues() {
+    assertEquals("-1", ScriptMeta.ScriptType.NORMAL_SCRIPT.getCode());
+    assertEquals("0", ScriptMeta.ScriptType.TRANSFORM_SCRIPT.getCode());
+    assertEquals("1", ScriptMeta.ScriptType.START_SCRIPT.getCode());
+    assertEquals("2", ScriptMeta.ScriptType.END_SCRIPT.getCode());
+    assertNotNull(ScriptMeta.ScriptType.TRANSFORM_SCRIPT.getDescription());
+  }
+}
diff --git 
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptTest.java
 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptTest.java
new file mode 100644
index 0000000000..c54200a5fe
--- /dev/null
+++ 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptTest.java
@@ -0,0 +1,434 @@
+/*
+ * 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.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaPluginType;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class ScriptTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<ScriptMeta, ScriptData> transformMockHelper;
+  private Script script;
+  private ScriptMeta meta;
+
+  @BeforeAll
+  static void initPlugins() throws HopException {
+    ValueMetaPluginType.getInstance().searchPlugins();
+  }
+
+  @BeforeEach
+  void setUp() {
+    transformMockHelper =
+        new TransformMockHelper<>("Script TEST", ScriptMeta.class, 
ScriptData.class);
+    when(transformMockHelper.logChannelFactory.create(any(), 
any(ILoggingObject.class)))
+        .thenReturn(transformMockHelper.iLogChannel);
+    when(transformMockHelper.pipeline.isRunning()).thenReturn(true);
+
+    meta = new ScriptMeta();
+    
when(transformMockHelper.iTransformMeta.getFields()).thenReturn(meta.getFields());
+    
when(transformMockHelper.iTransformMeta.getScripts()).thenReturn(meta.getScripts());
+    
when(transformMockHelper.iTransformMeta.getLanguageName()).thenReturn("Groovy");
+
+    script =
+        new Script(
+            transformMockHelper.transformMeta,
+            meta,
+            transformMockHelper.iTransformData,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+  }
+
+  @AfterEach
+  void tearDown() {
+    transformMockHelper.cleanUp();
+  }
+
+  // --- getValueFromScript: TYPE_INTEGER ---
+
+  @Test
+  void getValueFromScript_integer_returnsLong() throws HopValueException {
+    meta.getFields().add(createField("id", "Integer"));
+    assertEquals(42L, script.getValueFromScript(42, 0));
+    assertEquals(42L, script.getValueFromScript(Integer.valueOf(42), 0));
+  }
+
+  @Test
+  void getValueFromScript_long_returnsLong() throws HopValueException {
+    meta.getFields().add(createField("id", "Integer"));
+    assertEquals(42L, script.getValueFromScript(42L, 0));
+  }
+
+  @Test
+  void getValueFromScript_numberDouble_returnsLong() throws HopValueException {
+    meta.getFields().add(createField("id", "Integer"));
+    assertEquals(3L, script.getValueFromScript(3.7, 0));
+  }
+
+  @Test
+  void getValueFromScript_string_returnsLong() throws HopValueException {
+    meta.getFields().add(createField("id", "Integer"));
+    assertEquals(100L, script.getValueFromScript("100", 0));
+  }
+
+  @Test
+  void getValueFromScript_null_returnsNull() throws HopValueException {
+    meta.getFields().add(createField("id", "Integer"));
+    assertNull(script.getValueFromScript(null, 0));
+  }
+
+  @Test
+  void getValueFromScript_emptyName_throws() {
+    ScriptMeta.SField field = createField("", "Integer");
+    meta.getFields().add(field);
+    assertThrows(HopValueException.class, () -> script.getValueFromScript(1, 
0));
+  }
+
+  // --- getValueFromScript: TYPE_NUMBER ---
+
+  @Test
+  void getValueFromScript_numberType_integer_returnsDouble() throws 
HopValueException {
+    meta.getFields().add(createField("x", "Number"));
+    assertEquals(42.0, script.getValueFromScript(42, 0));
+  }
+
+  @Test
+  void getValueFromScript_numberType_double_returnsDouble() throws 
HopValueException {
+    meta.getFields().add(createField("x", "Number"));
+    assertEquals(3.14, (Double) script.getValueFromScript(3.14, 0), 1e-9);
+  }
+
+  @Test
+  void getValueFromScript_numberType_null_returnsNull() throws 
HopValueException {
+    meta.getFields().add(createField("x", "Number"));
+    assertNull(script.getValueFromScript(null, 0));
+  }
+
+  // --- getValueFromScript: TYPE_STRING ---
+
+  @Test
+  void getValueFromScript_stringType_returnsString() throws HopValueException {
+    meta.getFields().add(createField("name", "String"));
+    assertEquals("hello", script.getValueFromScript("hello", 0));
+  }
+
+  // --- getValueFromScript: TYPE_BOOLEAN ---
+
+  @Test
+  void getValueFromScript_booleanType_returnsBoolean() throws 
HopValueException {
+    meta.getFields().add(createField("flag", "Boolean"));
+    assertEquals(Boolean.TRUE, script.getValueFromScript(true, 0));
+  }
+
+  // --- getValueFromScript: TYPE_DATE ---
+
+  @Test
+  void getValueFromScript_dateType_date_returnsDate() throws HopValueException 
{
+    meta.getFields().add(createField("d", "Date"));
+    Date d = new Date(1234567890L);
+    assertEquals(d, script.getValueFromScript(d, 0));
+  }
+
+  @Test
+  void getValueFromScript_dateType_null_returnsNull() throws HopValueException 
{
+    meta.getFields().add(createField("d", "Date"));
+    assertNull(script.getValueFromScript(null, 0));
+  }
+
+  // --- getValueFromScript: TYPE_BIGNUMBER ---
+
+  @Test
+  void getValueFromScript_bignumberType_integer_returnsBigDecimal() throws 
HopValueException {
+    meta.getFields().add(createField("bn", "BigNumber"));
+    assertEquals(new BigDecimal("42"), script.getValueFromScript(42, 0));
+  }
+
+  @Test
+  void getValueFromScript_bignumberType_bigDecimal_returnsBigDecimal() throws 
HopValueException {
+    meta.getFields().add(createField("bn", "BigNumber"));
+    // Script.getValueFromScript TYPE_BIGNUMBER handles NativeNumber, 
NativeJavaObject, boxed
+    // primitives, String;
+    // java.math.BigDecimal falls through to default and throws (could be 
extended to handle
+    // BigDecimal).
+    BigDecimal bd = new BigDecimal("123.45");
+    assertThrows(HopValueException.class, () -> script.getValueFromScript(bd, 
0));
+  }
+
+  // --- putRow: Integer normalized to Long ---
+
+  @Test
+  void putRow_normalizesIntegerToLongForIntegerColumn() throws Exception {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("id"));
+    rowMeta.addValueMeta(new ValueMetaString("name"));
+
+    Object[] row = new Object[] {Integer.valueOf(99), "test"};
+
+    IRowSet outputSet = mock(IRowSet.class, Mockito.RETURNS_MOCKS);
+    when(outputSet.putRow(any(IRowMeta.class), 
any(Object[].class))).thenReturn(true);
+    when(outputSet.isDone()).thenReturn(false);
+    when(outputSet.getRowWait(anyLong(), 
any(TimeUnit.class))).thenReturn(null);
+
+    script.addRowSetToOutputRowSets(outputSet);
+    script.putRow(rowMeta, row);
+
+    ArgumentCaptor<Object[]> rowCaptor = 
ArgumentCaptor.forClass(Object[].class);
+    verify(outputSet).putRow(any(IRowMeta.class), rowCaptor.capture());
+    Object[] captured = rowCaptor.getValue();
+    assertInstanceOf(Long.class, captured[0], "Integer column should be 
normalized to Long");
+    assertEquals(99L, captured[0]);
+    assertEquals("test", captured[1]);
+  }
+
+  @Test
+  void putRow_doesNotModifyRowWhenNoIntegerColumn() throws Exception {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("name"));
+
+    Object[] row = new Object[] {"hello"};
+
+    IRowSet outputSet = mock(IRowSet.class, Mockito.RETURNS_MOCKS);
+    when(outputSet.putRow(any(IRowMeta.class), 
any(Object[].class))).thenReturn(true);
+    when(outputSet.isDone()).thenReturn(false);
+
+    script.addRowSetToOutputRowSets(outputSet);
+    script.putRow(rowMeta, row);
+
+    assertEquals("hello", row[0]);
+  }
+
+  @Test
+  void putRow_handlesNullRowMeta() throws Exception {
+    script.putRow(null, new Object[] {1});
+    // no exception when rowMeta is null (normalization loop skipped)
+  }
+
+  @Test
+  void getValueFromScript_indexOutOfBounds_throws() {
+    meta.getFields().add(createField("a", "Integer"));
+    assertThrows(IndexOutOfBoundsException.class, () -> 
script.getValueFromScript(1, 1));
+  }
+
+  // --- getValueFromScript: more branches ---
+
+  @Test
+  void getValueFromScript_numberType_parseFromToString() throws 
HopValueException {
+    meta.getFields().add(createField("x", "Number"));
+    Object obj =
+        new Object() {
+          @Override
+          public String toString() {
+            return "2.5";
+          }
+        };
+    assertEquals(2.5, (Double) script.getValueFromScript(obj, 0), 1e-9);
+  }
+
+  @Test
+  void getValueFromScript_integer_invalidString_throws() {
+    meta.getFields().add(createField("id", "Integer"));
+    assertThrows(HopValueException.class, () -> 
script.getValueFromScript("not-a-number", 0));
+  }
+
+  @Test
+  void getValueFromScript_stringType_nonStringObject_returnsResult() throws 
HopValueException {
+    meta.getFields().add(createField("name", "String"));
+    Integer value = 42;
+    assertEquals(value, script.getValueFromScript(value, 0));
+  }
+
+  @Test
+  void getValueFromScript_dateType_doubleTimestamp_returnsDate() throws 
HopValueException {
+    meta.getFields().add(createField("d", "Date"));
+    long ts = 1234567890L;
+    assertEquals(new Date(ts), script.getValueFromScript((double) ts, 0));
+  }
+
+  @Test
+  void getValueFromScript_dateType_numericString_returnsDate() throws 
HopValueException {
+    meta.getFields().add(createField("d", "Date"));
+    long ts = 1000000L;
+    Date expected = new Date(ts);
+    assertEquals(expected, script.getValueFromScript(String.valueOf(ts), 0));
+  }
+
+  @Test
+  void getValueFromScript_bignumberType_string_returnsBigDecimal() throws 
HopValueException {
+    meta.getFields().add(createField("bn", "BigNumber"));
+    assertEquals(new BigDecimal("999"), script.getValueFromScript("999", 0));
+  }
+
+  @Test
+  void getValueFromScript_binaryType_returnsResult() throws HopValueException {
+    meta.getFields().add(createField("bin", "Binary"));
+    byte[] bytes = new byte[] {1, 2, 3};
+    assertEquals(bytes, script.getValueFromScript(bytes, 0));
+  }
+
+  @Test
+  void getValueFromScript_typeNone_throws() {
+    ScriptMeta.SField field = createField("x", "NoSuchType");
+    meta.getFields().add(field);
+    assertThrows(HopValueException.class, () -> script.getValueFromScript(1, 
0));
+  }
+
+  @Test
+  void init_setsEngineWhenLanguageAvailable() throws Exception {
+    ScriptData data = new ScriptData();
+    meta.setLanguageName("Groovy");
+    Script scriptWithData =
+        new Script(
+            transformMockHelper.transformMeta,
+            meta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    assertTrue(scriptWithData.init());
+    assertNotNull(data.engine);
+  }
+
+  @Test
+  void init_stillReturnsTrueWhenEngineLookupFails() throws Exception {
+    ScriptData data = new ScriptData();
+    meta.setLanguageName("NonExistentLanguageXYZ");
+    Script scriptWithData =
+        new Script(
+            transformMockHelper.transformMeta,
+            meta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    assertTrue(scriptWithData.init());
+    assertNull(data.engine);
+  }
+
+  @Test
+  void init_copiesScriptsFromMeta() throws Exception {
+    ScriptData data = new ScriptData();
+    meta.setLanguageName("Groovy");
+    meta.getScripts()
+        .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, 
"s1", "x=1"));
+    meta.getScripts()
+        .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.START_SCRIPT, 
"start", "y=0"));
+    meta.getScripts().add(new 
ScriptMeta.SScript(ScriptMeta.ScriptType.END_SCRIPT, "end", "z=2"));
+    Script scriptWithData =
+        new Script(
+            transformMockHelper.transformMeta,
+            meta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    assertTrue(scriptWithData.init());
+    assertNotNull(data.engine);
+  }
+
+  @Test
+  void putRow_nullRow_doesNotThrow() throws Exception {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("id"));
+    script.putRow(rowMeta, null);
+  }
+
+  /** Runs one row through the transform: addValues, eval script, putRow. */
+  @Test
+  void processRow_oneRow_producesOutputRow() throws Exception {
+    ScriptData data = new ScriptData();
+    meta.setLanguageName("Groovy");
+    meta.getScripts()
+        .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, 
"s1", "out=1"));
+    ScriptMeta.SField outField = createField("out", "Integer");
+    outField.setRename("out");
+    meta.getFields().add(outField);
+
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("dummy"));
+    Object[] inputRow = new Object[] {"x"};
+
+    Script runScript =
+        new Script(
+            transformMockHelper.transformMeta,
+            meta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    runScript.addRowSetToInputRowSets(
+        
transformMockHelper.getMockInputRowSet(TransformMockHelper.asList(inputRow)));
+    IRowSet outputSet = mock(IRowSet.class, Mockito.RETURNS_MOCKS);
+    when(outputSet.putRow(any(IRowMeta.class), 
any(Object[].class))).thenReturn(true);
+    when(outputSet.isDone()).thenReturn(false);
+    when(outputSet.getRowWait(anyLong(), 
any(TimeUnit.class))).thenReturn(null);
+    runScript.addRowSetToOutputRowSets(outputSet);
+
+    assertTrue(runScript.init());
+    assertTrue(runScript.processRow());
+    runScript.processRow();
+
+    ArgumentCaptor<Object[]> rowCaptor = 
ArgumentCaptor.forClass(Object[].class);
+    ArgumentCaptor<IRowMeta> metaCaptor = 
ArgumentCaptor.forClass(IRowMeta.class);
+    verify(outputSet).putRow(metaCaptor.capture(), rowCaptor.capture());
+    Object[] outRow = rowCaptor.getValue();
+    assertNotNull(outRow);
+    assertEquals(1, outRow.length);
+    assertEquals(1L, outRow[0]);
+  }
+
+  private static ScriptMeta.SField createField(String name, String type) {
+    ScriptMeta.SField f = new ScriptMeta.SField();
+    f.setName(name);
+    f.setType(type);
+    return f;
+  }
+}
diff --git 
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptUtilsTest.java
 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptUtilsTest.java
new file mode 100644
index 0000000000..59450c433e
--- /dev/null
+++ 
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptUtilsTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import org.junit.jupiter.api.Test;
+
+class ScriptUtilsTest {
+
+  @Test
+  void getInstance_returnsSingleton() {
+    ScriptUtils a = ScriptUtils.getInstance();
+    ScriptUtils b = ScriptUtils.getInstance();
+    assertNotNull(a);
+    assertSame(a, b);
+  }
+
+  @Test
+  void getScriptLanguageNames_returnsNonEmptyList() {
+    assertNotNull(ScriptUtils.getInstance().getScriptLanguageNames());
+    assertTrue(ScriptUtils.getInstance().getScriptLanguageNames().size() > 0);
+  }
+
+  @Test
+  void getScriptLanguageNames_containsGroovy() {
+    assertTrue(
+        ScriptUtils.getInstance().getScriptLanguageNames().contains("Groovy"),
+        "Expected Groovy in script language names");
+  }
+
+  @Test
+  void getScriptLanguageNames_containsECMAScript() {
+    assertTrue(
+        
ScriptUtils.getInstance().getScriptLanguageNames().contains("ECMAScript"),
+        "Expected ECMAScript in script language names (GraalVM JS)");
+  }
+
+  @Test
+  void getScriptEngineByName_groovy_returnsEngine() {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("Groovy");
+    assertNotNull(engine);
+  }
+
+  @Test
+  void getScriptEngineByName_groovy_evalSucceeds() throws ScriptException {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("Groovy");
+    assertNotNull(engine);
+    Object result = engine.eval("1 + 2");
+    assertEquals(3, result);
+  }
+
+  @Test
+  void getScriptEngineByName_ecmascript_returnsEngine() {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("ECMAScript");
+    assertNotNull(engine);
+  }
+
+  @Test
+  void getScriptEngineByName_ecmascript_evalSucceeds() throws ScriptException {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("ECMAScript");
+    assertNotNull(engine);
+    Object result = engine.eval("1 + 2");
+    assertNotNull(result);
+    assertTrue(result instanceof Number, "Expected number, got " + 
result.getClass().getName());
+    assertEquals(3, ((Number) result).intValue());
+  }
+
+  @Test
+  void getScriptEngineByName_ecmascript_returnsGraalEngineWithHostAccess() 
throws ScriptException {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("ECMAScript");
+    assertNotNull(engine);
+    assertTrue(
+        engine instanceof GraalJSScriptEngine, "ECMAScript engine should be 
GraalJSScriptEngine");
+
+    // Verify host access: script can call a method on a bound Java object 
(e.g. transform.logBasic)
+    engine.put("holder", new StringBuilder());
+    engine.eval("holder.append('ok')");
+    assertEquals("ok", engine.get("holder").toString());
+  }
+
+  @Test
+  void getScriptEngineByName_unknown_returnsNull() {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("NonExistentLanguageXYZ");
+    assertNull(engine);
+  }
+
+  @Test
+  void getScriptEngineByName_python_returnsEngine() {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("python");
+    assertNotNull(engine);
+  }
+
+  @Test
+  void getScriptEngineByName_python_evalSucceeds() throws ScriptException {
+    ScriptEngine engine = 
ScriptUtils.getInstance().getScriptEngineByName("python");
+    assertNotNull(engine);
+    Object result = engine.eval("1 + 2");
+    assertEquals(3, result);
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
index c13fcca223..42674e58a7 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
@@ -127,6 +127,96 @@ public enum ScriptEngine {
           "vars",
           "zip",
           "__import__")),
+  ECMASCRIPT(
+      List.of(
+          "async",
+          "await",
+          "break",
+          "case",
+          "catch",
+          "class",
+          "const",
+          "continue",
+          "debugger",
+          "default",
+          "delete",
+          "do",
+          "else",
+          "export",
+          "extends",
+          "false",
+          "finally",
+          "for",
+          "function",
+          "if",
+          "import",
+          "in",
+          "instanceof",
+          "let",
+          "new",
+          "null",
+          "return",
+          "super",
+          "switch",
+          "this",
+          "throw",
+          "true",
+          "try",
+          "typeof",
+          "var",
+          "void",
+          "while",
+          "yield"),
+      List.of(
+          "Array",
+          "ArrayBuffer",
+          "BigInt",
+          "Boolean",
+          "DataView",
+          "Date",
+          "Error",
+          "EvalError",
+          "Float32Array",
+          "Float64Array",
+          "Function",
+          "Infinity",
+          "Int8Array",
+          "Int16Array",
+          "Int32Array",
+          "Intl",
+          "JSON",
+          "Map",
+          "Math",
+          "NaN",
+          "Number",
+          "Object",
+          "Promise",
+          "Proxy",
+          "RangeError",
+          "ReferenceError",
+          "Reflect",
+          "RegExp",
+          "Set",
+          "String",
+          "Symbol",
+          "SyntaxError",
+          "TypeError",
+          "Uint8Array",
+          "Uint16Array",
+          "Uint32Array",
+          "URIError",
+          "WeakMap",
+          "WeakSet",
+          "decodeURI",
+          "decodeURIComponent",
+          "encodeURI",
+          "encodeURIComponent",
+          "eval",
+          "isFinite",
+          "isNaN",
+          "parseFloat",
+          "parseInt",
+          "undefined")),
   GROOVY(
       List.of(
           "as",


Reply via email to