diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml
index 5a5ff68b054cc..dde3319bf46d7 100644
--- a/bundles/org.openhab.automation.jsscripting/pom.xml
+++ b/bundles/org.openhab.automation.jsscripting/pom.xml
@@ -25,6 +25,7 @@
21.3.0
6.2.1
${project.version}
+ openhab@0.0.1-beta.3
@@ -44,6 +45,62 @@
+
+ com.github.eirslett
+ frontend-maven-plugin
+ 1.12.0
+
+ v12.16.1
+ target/js
+
+
+
+ Install node and npm
+
+ install-node-and-npm
+
+ generate-sources
+
+
+ npm install
+
+ npm
+
+
+ install ${ohjs.version} webpack webpack-cli
+
+
+
+ npx webpack
+
+ npx
+
+
+ webpack -c ./node_modules/openhab/webpack.config.js --entry ./node_modules/openhab/ -o ./dist
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+
+ add-resource
+
+ generate-sources
+
+
+
+ target/js/dist
+ node_modules
+
+
+
+
+
+
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
index d6b98efeb7382..4fa8ac8d98003 100644
--- 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
@@ -20,15 +20,26 @@
import javax.script.ScriptEngine;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.openhab.core.config.core.ConfigurableService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
/**
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
*
* @author Jonathan Gilbert - Initial contribution
+ * @author Dan Cunningham - Script injections
*/
-@Component(service = ScriptEngineFactory.class)
+@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.automation.jsscripting", property = Constants.SERVICE_PID
+ + "=org.openhab.automation.jsscripting")
+@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
+ private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
+ private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
+ private boolean injectionEnabled;
public static final String MIME_TYPE = "application/javascript;version=ECMAScript-2021";
@@ -59,7 +70,18 @@ public void scopeValues(ScriptEngine scriptEngine, Map scopeValu
@Override
public ScriptEngine createScriptEngine(String scriptType) {
- OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
- return new DebuggingGraalScriptEngine<>(engine);
+ return new DebuggingGraalScriptEngine<>(
+ new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null));
+ }
+
+ @Activate
+ protected void activate(BundleContext context, Map config) {
+ modified(config);
+ }
+
+ @Modified
+ protected void modified(Map config) {
+ Object injectionEnabled = config.get(CFG_INJECTION_ENABLED);
+ this.injectionEnabled = injectionEnabled == null || (Boolean) injectionEnabled;
}
}
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
index 6621d5d1a1578..f4939baa0f08b 100644
--- 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
@@ -15,22 +15,32 @@
import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
import java.nio.file.FileSystems;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
+import java.util.Collections;
+import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.script.ScriptContext;
+import javax.script.ScriptException;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
+import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel;
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
@@ -43,32 +53,36 @@
* GraalJS Script Engine implementation
*
* @author Jonathan Gilbert - Initial contribution
+ * @author Dan Cunningham - Script injections
*/
public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
-
+ private static final String GLOBAL_REQUIRE = "require(\"@jsscripting-globals\");";
private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
+ // final CommonJS search path for our library
+ private static final Path LOCAL_NODE_PATH = Paths.get("/node_modules");
// these fields start as null because they are populated on first use
private @NonNullByDefault({}) String engineIdentifier;
private @NonNullByDefault({}) Consumer scriptDependencyListener;
private boolean initialized = false;
+ private String globalScript;
/**
* 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() {
+ public OpenhabGraalJSScriptEngine(@Nullable String injectionCode) {
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
+ this.globalScript = GLOBAL_REQUIRE + (injectionCode != null ? injectionCode : "");
delegate = GraalJSScriptEngine.create(
Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
.build(),
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
.option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
- .option("js.nashorn-compat", "true") // to ease
- // migration
+ .option("js.nashorn-compat", "true") // to ease migration
.option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
// want ecma2021
.option("js.commonjs-require", "true") // enable CommonJS module support
@@ -80,15 +94,52 @@ public SeekableByteChannel newByteChannel(Path path, Set extends OpenOption> o
if (scriptDependencyListener != null) {
scriptDependencyListener.accept(path.toString());
}
-
if (path.toString().endsWith(".js")) {
+ SeekableByteChannel sbc = null;
+ if (path.startsWith(LOCAL_NODE_PATH)) {
+ InputStream is = getClass().getResourceAsStream(path.toString());
+ if (is == null) {
+ throw new IOException("Could not read " + path.toString());
+ }
+ sbc = new ReadOnlySeekableByteArrayChannel(is.readAllBytes());
+ } else {
+ sbc = super.newByteChannel(path, options, attrs);
+ }
return new PrefixedSeekableByteChannel(
- ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
- super.newByteChannel(path, options, attrs));
+ ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(), sbc);
} else {
return super.newByteChannel(path, options, attrs);
}
}
+
+ @Override
+ public void checkAccess(Path path, Set extends AccessMode> modes,
+ LinkOption... linkOptions) throws IOException {
+ if (path.startsWith(LOCAL_NODE_PATH)) {
+ if (getClass().getResource(path.toString()) == null) {
+ throw new NoSuchFileException(path.toString());
+ }
+ } else {
+ super.checkAccess(path, modes, linkOptions);
+ }
+ }
+
+ @Override
+ public Map readAttributes(Path path, String attributes,
+ LinkOption... options) throws IOException {
+ if (path.startsWith(LOCAL_NODE_PATH)) {
+ return Collections.singletonMap("isRegularFile", true);
+ }
+ return super.readAttributes(path, attributes, options);
+ }
+
+ @Override
+ public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException {
+ if (path.startsWith(LOCAL_NODE_PATH)) {
+ return path;
+ }
+ return super.toRealPath(path, linkOptions);
+ }
}));
}
@@ -130,5 +181,11 @@ protected void beforeInvocation() {
delegate.put("require", wrapRequireFn.apply((Function