diff --git a/CODEOWNERS b/CODEOWNERS
index 96f873cd0c5c0..666bd08184ced 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -7,6 +7,7 @@
# Add-on maintainers:
/bundles/org.openhab.automation.groovyscripting/ @wborn
/bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers
+/bundles/org.openhab.automation.jsscripting/ @jpg0
/bundles/org.openhab.binding.adorne/ @theiding
/bundles/org.openhab.binding.airquality/ @kubawolanin
/bundles/org.openhab.binding.airvisualnode/ @3cky
diff --git a/bundles/org.openhab.automation.jsscripting/.classpath b/bundles/org.openhab.automation.jsscripting/.classpath
new file mode 100644
index 0000000000000..a5d95095ccaaf
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.automation.jsscripting/.project b/bundles/org.openhab.automation.jsscripting/.project
new file mode 100644
index 0000000000000..277326b68bb9c
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.automation.jsscripting
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.automation.jsscripting/NOTICE b/bundles/org.openhab.automation.jsscripting/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.automation.jsscripting/README.md b/bundles/org.openhab.automation.jsscripting/README.md
new file mode 100644
index 0000000000000..51b2f237c80a0
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/README.md
@@ -0,0 +1,3 @@
+# JSScripting
+
+TODO
\ No newline at end of file
diff --git a/bundles/org.openhab.automation.jsscripting/bnd.bnd b/bundles/org.openhab.automation.jsscripting/bnd.bnd
new file mode 100644
index 0000000000000..47c2bad3c8d92
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/bnd.bnd
@@ -0,0 +1,13 @@
+Bundle-SymbolicName: ${project.artifactId}
+DynamicImport-Package: *
+Import-Package: org.openhab.core.automation.module.script,javax.management,javax.script,javax.xml.datatype,javax.xml.stream;version="[1.0,2)",org.osgi.framework;version="[1.8,2)",org.slf4j;version="[1.7,2)"
+Require-Capability: osgi.extender;
+ filter:="(osgi.extender=osgi.serviceloader.processor)",
+ osgi.serviceloader;
+ filter:="(osgi.serviceloader=org.graalvm.polyglot.impl.AbstractPolyglotImpl)";
+ cardinality:=multiple
+
+SPI-Provider: *
+SPI-Consumer: *
+
+-fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning
diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml
new file mode 100644
index 0000000000000..61b1912a72a1b
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/pom.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.0.0-SNAPSHOT
+
+
+ org.openhab.automation.jsscripting
+
+ openHAB Add-ons :: Bundles :: Automation :: JSScripting
+
+
+
+ !sun.misc.*,
+ !sun.reflect.*,
+ !com.sun.management.*,
+ !jdk.internal.reflect.*,
+ !jdk.vm.ci.services
+
+ 20.1.0
+ 6.2.1
+ ${project.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ embed-dependencies
+
+ unpack-dependencies
+
+
+ META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider
+
+
+
+
+
+
+
+
+
+ org.graalvm.truffle
+ truffle-api
+ ${graal.version}
+
+
+ org.graalvm.js
+ js-scriptengine
+ ${graal.version}
+
+
+ org.graalvm.js
+ js-launcher
+ ${graal.version}
+
+
+ org.graalvm.sdk
+ graal-sdk
+ ${graal.version}
+
+
+ org.graalvm.regex
+ regex
+ ${graal.version}
+
+
+ org.graalvm.js
+ js
+ ${graal.version}
+
+
+ com.ibm.icu
+ icu4j
+ 62.1
+
+
+
+
+ org.ow2.asm
+ asm
+ ${asm.version}
+
+
+ org.ow2.asm
+ asm-commons
+ ${asm.version}
+
+
+ org.ow2.asm
+ asm-tree
+ ${asm.version}
+
+
+ org.ow2.asm
+ asm-util
+ ${asm.version}
+
+
+ org.ow2.asm
+ asm-analysis
+ ${asm.version}
+
+
+
+ org.openhab.core.bom
+ org.openhab.core.bom.compile
+ pom
+ provided
+
+
+ org.openhab.core.bom
+ org.openhab.core.bom.openhab-core
+ pom
+ provided
+
+
+
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml b/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..1c046da270965
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.automation.jsscripting/${project.version}
+
+
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java
new file mode 100644
index 0000000000000..836b8b931b405
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.automation.jsscripting;
+
+import com.oracle.truffle.js.runtime.java.adapter.JavaAdapterFactory;
+
+/**
+ * Class utility to allow creation of 'extendable' classes with a classloader of the GraalJS bundle, rather than the
+ * classloader of the file being extended.
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public class ClassExtender {
+ private static ClassLoader classLoader = ClassExtender.class.getClassLoader();
+
+ public static Object extend(String className) {
+ try {
+ return extend(Class.forName(className));
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Cannot find class " + className, e);
+ }
+ }
+
+ public static Object extend(Class> clazz) {
+ return JavaAdapterFactory.getAdapterClassFor(clazz, null, classLoader);
+ }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java
new file mode 100644
index 0000000000000..7e094d7348a94
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.automation.jsscripting.internal;
+
+import javax.script.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.graalvm.polyglot.PolyglotException;
+import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wraps ScriptEngines provided by Graal to provide error messages and stack traces for scripts.
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+@NonNullByDefault
+class DebuggingGraalScriptEngine
+ extends InvocationInterceptingScriptEngineWithInvocable {
+
+ private static final Logger stackLogger = LoggerFactory.getLogger("org.openhab.automation.script.javascript.stack");
+
+ public DebuggingGraalScriptEngine(T delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ScriptException afterThrowsInvocation(ScriptException se) {
+ Throwable cause = se.getCause();
+ if (cause instanceof PolyglotException) {
+ stackLogger.error("Failed to execute script:", cause);
+ }
+ return se;
+ }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java
new file mode 100644
index 0000000000000..2a5682636a94d
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.automation.jsscripting.internal;
+
+import java.util.*;
+
+import javax.script.ScriptEngine;
+
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.osgi.service.component.annotations.Component;
+
+import com.oracle.truffle.js.scriptengine.GraalJSEngineFactory;
+
+/**
+ * An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+@Component(service = ScriptEngineFactory.class)
+public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
+
+ @Override
+ public List getScriptTypes() {
+ List scriptTypes = new ArrayList<>();
+ GraalJSEngineFactory graalJSEngineFactory = new GraalJSEngineFactory();
+
+ scriptTypes.addAll(graalJSEngineFactory.getMimeTypes());
+ scriptTypes.addAll(graalJSEngineFactory.getExtensions());
+
+ return Collections.unmodifiableList(scriptTypes);
+ }
+
+ @Override
+ public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) {
+ // noop; the are retrieved via modules, not injected
+ }
+
+ @Override
+ public ScriptEngine createScriptEngine(String scriptType) {
+ OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
+ return new DebuggingGraalScriptEngine<>(engine);
+ }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java
new file mode 100644
index 0000000000000..ec54afb7df085
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.automation.jsscripting.internal;
+
+import java.util.Optional;
+
+import org.graalvm.polyglot.Value;
+
+/**
+ * Locates modules from a module name
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public interface ModuleLocator {
+ Optional locateModule(String name);
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java
new file mode 100644
index 0000000000000..39623c50a8c75
--- /dev/null
+++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.automation.jsscripting.internal;
+
+import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.FileSystems;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileAttribute;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javax.script.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.graalvm.polyglot.Context;
+import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
+import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
+import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
+
+/**
+ * GraalJS Script Engine implementation
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable {
+
+ private static final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
+
+ private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
+ private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
+ "javascript", "personal");
+
+ // these fields start as null because they are populated on first use
+ @NonNullByDefault({})
+ private String engineIdentifier;
+ @NonNullByDefault({})
+ private Consumer scriptDependencyListener;
+
+ private boolean initialized = false;
+
+ /**
+ * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
+ * lifecycle and provides hooks for scripts to do so too.
+ */
+ public OpenhabGraalJSScriptEngine() {
+ super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
+ delegate = GraalJSScriptEngine.create(null,
+ Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
+ .option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease
+ // migration
+ .option("js.commonjs-require", "true") // enable CommonJS module support
+ .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
+ @Override
+ public SeekableByteChannel newByteChannel(Path path, Set extends OpenOption> options,
+ FileAttribute>... attrs) throws IOException {
+ if (scriptDependencyListener != null) {
+ scriptDependencyListener.accept(path.toString());
+ }
+
+ if (path.toString().endsWith(".js")) {
+ return new PrefixedSeekableByteChannel(
+ ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
+ super.newByteChannel(path, options, attrs));
+ } else {
+ return super.newByteChannel(path, options, attrs);
+ }
+ }
+ }));
+ }
+
+ @Override
+ protected void beforeInvocation() {
+ if (initialized)
+ return;
+
+ ScriptContext ctx = delegate.getContext();
+
+ // these are added post-construction, so we need to fetch them late
+ this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
+ if (this.engineIdentifier == null) {
+ throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings");
+ }
+
+ ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
+ .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
+ if (scriptExtensionAccessor == null) {
+ throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings");
+ }
+
+ scriptDependencyListener = (Consumer) ctx
+ .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
+ if (scriptDependencyListener == null) {
+ logger.warn(
+ "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
+ }
+
+ ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
+ scriptExtensionAccessor);
+
+ Function, Function> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
+ .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
+ .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
+
+ delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
+ delegate.put("require", wrapRequireFn.apply((Function