Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/src/main/java/org/densy/scriptify/api/script/Script.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.densy.scriptify.api.exception.ScriptFunctionException;
import org.densy.scriptify.api.script.constant.ScriptConstantManager;
import org.densy.scriptify.api.script.function.ScriptFunctionManager;
import org.densy.scriptify.api.script.module.ScriptModuleManager;
import org.densy.scriptify.api.script.security.ScriptSecurityManager;

/**
Expand All @@ -21,6 +22,8 @@ public interface Script<T> {
*/
ScriptSecurityManager getSecurityManager();

ScriptModuleManager getModuleManager();

/**
* Retrieves the function manager associated with this script.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.densy.scriptify.api.script.module;

import org.densy.scriptify.api.script.module.export.ScriptExport;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;

import java.util.Collection;

/**
* A module that exports elements to the script environment.
* Named modules (non-null name) are accessible via ES import.
* The global module (null name) injects exports directly into global scope.
*/
public interface ScriptModule {

/**
* Module name for ES import, e.g. "@densy/mymodule".
*/
@NotNull
String getName();

@UnmodifiableView
Collection<ScriptExport> getExports();

void export(ScriptExport export);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.densy.scriptify.api.script.module;

/**
* Manages all modules available to the script.
* The global module is always present and created automatically.
*/
public interface ScriptModuleManager {

/**
* Exports added here are available globally without import
*/
ScriptModule getGlobalModule();

ScriptModule getModule(String name);

void addModule(ScriptModule module);

void removeModule(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.densy.scriptify.api.script.module.export;

/**
* Represents any exportable element from a module.
*/
public interface ScriptExport {
String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.densy.scriptify.api.script.module.export;

import lombok.Getter;

/**
* Universal wrapper for exporting Java values and classes.
*
* <pre>
* new ScriptValueExport("PI", 3.14) - PI available as a number
* new ScriptValueExport("MyClass", MyClass.class) - new MyClass() in JS
* new ScriptValueExport("service", myService) - access to instance methods
* </pre>
*/
@Getter
public class ScriptValueExport implements ScriptExport {

private final String name;
private final Object value;

public ScriptValueExport(String name, Object value) {
this.name = name;
this.value = value;
}

@Override
public String getName() {
return name;
}

public boolean isClass() {
return value instanceof Class<?>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.densy.scriptify.core.script.module;

import org.densy.scriptify.api.script.module.export.ScriptExport;
import org.densy.scriptify.api.script.module.ScriptModule;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public abstract class AbstractScriptModule implements ScriptModule {
private final Map<String, ScriptExport> exports = new LinkedHashMap<>();

@Override
public void export(ScriptExport export) {
if (export == null) {
throw new IllegalArgumentException("Export cannot be null");
}
exports.put(export.getName(), export);
}

@Override
public Collection<ScriptExport> getExports() {
return Collections.unmodifiableCollection(exports.values());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.densy.scriptify.core.script.module;

import org.jetbrains.annotations.NotNull;

public class ScriptGlobalModule extends AbstractScriptModule {
@Override
public @NotNull String getName() {
return "global";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.densy.scriptify.core.script.module;

import org.jetbrains.annotations.NotNull;

import java.util.Objects;

public class SimpleScriptModule extends AbstractScriptModule {
private final String name;

public SimpleScriptModule(String name) {
this.name = Objects.requireNonNull(name, "Module name cannot be null");
}

@Override
public @NotNull String getName() {
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.densy.scriptify.core.script.module.export;

import lombok.Getter;
import org.densy.scriptify.api.script.constant.ScriptConstant;
import org.densy.scriptify.api.script.module.export.ScriptExport;

@Getter
public final class ScriptConstantExport implements ScriptExport {

private final ScriptConstant constant;

public ScriptConstantExport(ScriptConstant constant) {
this.constant = constant;
}

@Override
public String getName() {
return constant.getName();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.densy.scriptify.core.script.module.export;

import lombok.Getter;
import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition;
import org.densy.scriptify.api.script.module.export.ScriptExport;

@Getter
public final class ScriptFunctionExport implements ScriptExport {

private final ScriptFunctionDefinition definition;

public ScriptFunctionExport(ScriptFunctionDefinition definition) {
this.definition = definition;
}

@Override
public String getName() {
return definition.getFunction().getName();
}

}
1 change: 1 addition & 0 deletions script-js-graalvm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ repositories {

dependencies {
api(project(":core"))
api(project(":common"))
api("org.graalvm.polyglot:polyglot:24.1.1")
api("org.graalvm.polyglot:js:24.1.1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,47 @@
import org.densy.scriptify.api.script.constant.ScriptConstantManager;
import org.densy.scriptify.api.script.function.ScriptFunctionManager;
import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition;
import org.densy.scriptify.api.script.module.ScriptModuleManager;
import org.densy.scriptify.api.script.security.ScriptSecurityManager;
import org.densy.scriptify.core.script.constant.StandardConstantManager;
import org.densy.scriptify.core.script.function.StandardFunctionManager;
import org.densy.scriptify.core.script.module.export.ScriptConstantExport;
import org.densy.scriptify.core.script.module.export.ScriptFunctionExport;
import org.densy.scriptify.core.script.security.StandardSecurityManager;
import org.densy.scriptify.js.graalvm.script.module.GraalModuleManager;
import org.densy.scriptify.js.graalvm.script.module.fs.VirtualModuleFileSystem;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.io.IOAccess;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

public class JsScript implements Script<Value> {

private final ScriptSecurityManager securityManager = new StandardSecurityManager();
private final GraalModuleManager moduleManager = new GraalModuleManager(this);
private ScriptFunctionManager functionManager = new StandardFunctionManager();
private ScriptConstantManager constantManager = new StandardConstantManager();
private final List<String> extraScript = new ArrayList<>();

@Override
public ScriptConstantManager getConstantManager() {
return constantManager;
public ScriptSecurityManager getSecurityManager() {
return securityManager;
}

@Override
public ScriptSecurityManager getSecurityManager() {
return securityManager;
public ScriptModuleManager getModuleManager() {
return moduleManager;
}

@Override
public ScriptConstantManager getConstantManager() {
return constantManager;
}

@Override
Expand All @@ -59,6 +73,10 @@ public void addExtraScript(String script) {

@Override
public CompiledScript<Value> compile(String script) throws ScriptException {
// A context reference, so that once it has been created,
// we can access it in the file system
AtomicReference<Context> contextRef = new AtomicReference<>();

Context.Builder builder = Context.newBuilder("js")
.allowHostAccess(HostAccess.newBuilder(HostAccess.ALL)
// Mapping for the ScriptObject class required
Expand All @@ -69,6 +87,9 @@ public CompiledScript<Value> compile(String script) throws ScriptException {
object -> true,
ScriptObject::getValue
)
.build())
.allowIO(IOAccess.newBuilder()
.fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get))
.build());

// If security mode is enabled, search all exclusions
Expand All @@ -80,16 +101,15 @@ public CompiledScript<Value> compile(String script) throws ScriptException {
}

Context context = builder.build();

Value bindings = context.getBindings("js");
contextRef.set(context);

for (ScriptFunctionDefinition definition : functionManager.getFunctions().values()) {
bindings.putMember(definition.getFunction().getName(), new JsFunction(this, definition));
moduleManager.getGlobalModule().export(new ScriptFunctionExport(definition));
}

for (ScriptConstant constant : constantManager.getConstants().values()) {
bindings.putMember(constant.getName(), constant.getValue());
moduleManager.getGlobalModule().export(new ScriptConstantExport(constant));
}
moduleManager.applyTo(context);

// Building full script including extra script code
StringBuilder fullScript = new StringBuilder();
Expand All @@ -99,7 +119,11 @@ public CompiledScript<Value> compile(String script) throws ScriptException {
fullScript.append(script);

try {
return new JsCompiledScript(context, context.eval("js", fullScript.toString()));
Source source = Source.newBuilder("js", fullScript.toString(), "script.mjs")
.mimeType("application/javascript+module")
.build();

return new JsCompiledScript(context, context.eval(source));
} catch (Exception e) {
throw new ScriptException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.densy.scriptify.js.graalvm.script.module;

import org.densy.scriptify.api.script.Script;
import org.densy.scriptify.api.script.module.ScriptModule;
import org.densy.scriptify.api.script.module.ScriptModuleManager;
import org.densy.scriptify.api.script.module.export.ScriptExport;
import org.densy.scriptify.api.script.module.export.ScriptValueExport;
import org.densy.scriptify.core.script.module.ScriptGlobalModule;
import org.densy.scriptify.core.script.module.export.ScriptConstantExport;
import org.densy.scriptify.core.script.module.export.ScriptFunctionExport;
import org.densy.scriptify.js.graalvm.script.JsFunction;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public class GraalModuleManager implements ScriptModuleManager {

private final Script<?> script;
private final ScriptGlobalModule globalModule = new ScriptGlobalModule();
private final Map<String, ScriptModule> modules = new LinkedHashMap<>();

public GraalModuleManager(Script<?> script) {
this.script = script;
}

@Override
public ScriptGlobalModule getGlobalModule() {
return globalModule;
}

@Override
public ScriptModule getModule(String name) {
return modules.get(name);
}

@Override
public void addModule(ScriptModule module) {
Objects.requireNonNull(module, "module cannot be null");
Objects.requireNonNull(module.getName(), "module name cannot be null");
modules.put(module.getName(), module);
}

@Override
public void removeModule(String name) {
modules.remove(name);
}

public void applyTo(Context context) {
Value bindings = context.getBindings("js");

for (ScriptExport export : globalModule.getExports()) {
bindings.putMember(export.getName(), resolveValue(context, export));
}
}

private Object resolveValue(Context context, ScriptExport export) {
if (export instanceof ScriptValueExport valueExport) {
return context.asValue(valueExport.getValue());
}
if (export instanceof ScriptFunctionExport functionExport) {
return new JsFunction(script, functionExport.getDefinition());
}
if (export instanceof ScriptConstantExport constantExport) {
return constantExport.getConstant().getValue();
}
throw new UnsupportedOperationException("Unsupported export type: " + export.getClass().getName() );
}
}
Loading
Loading