diff --git a/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java new file mode 100644 index 0000000..f414ed7 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java @@ -0,0 +1,35 @@ +package org.densy.scriptify.api.exception; + +/** + * Custom exception for errors while script copy process. + */ +public class ScriptModuleCopyException extends RuntimeException { + + /** + * Creates a new ScriptModuleCopyException with the specified message. + * + * @param message the detail message + */ + public ScriptModuleCopyException(String message) { + super(message); + } + + /** + * Creates a new ScriptModuleCopyException with the specified message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ + public ScriptModuleCopyException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new ScriptModuleCopyException with the specified cause. + * + * @param cause the cause of the exception + */ + public ScriptModuleCopyException(Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java new file mode 100644 index 0000000..5e3695c --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java @@ -0,0 +1,17 @@ +package org.densy.scriptify.api.exception; + +/** + * Custom exception for errors when script module context mismatch occurs. + */ +public class ScriptModuleWrongContextException extends ScriptException { + + /** + * Creates a new ScriptModuleWrongContextException with the specified message. + * + * @param expected the expected context + * @param actual the given context + */ + public ScriptModuleWrongContextException(Class expected, Class actual) { + super("Expected context of type " + expected.getName() + " but got " + actual.getName()); + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/Script.java b/api/src/main/java/org/densy/scriptify/api/script/Script.java index 7db08a5..7bbba65 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/Script.java +++ b/api/src/main/java/org/densy/scriptify/api/script/Script.java @@ -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; /** @@ -21,6 +22,8 @@ public interface Script { */ ScriptSecurityManager getSecurityManager(); + ScriptModuleManager getModuleManager(); + /** * Retrieves the function manager associated with this script. * diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java new file mode 100644 index 0000000..7818789 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java @@ -0,0 +1,39 @@ +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. Modules can be accessed via ES import (in JavaScript). + */ +public interface ScriptModule { + + /** + * Module name for ES import, e.g. "@densy/mymodule". + */ + @NotNull String getName(); + + /** + * Gets collection of all exports in module. + * + * @return Collection with ScriptExport + */ + @UnmodifiableView Collection getExports(); + + /** + * Adds export to the module. + * + * @param export export to add + */ + void export(ScriptExport export); + + /** + * Copies all exports from the target module that are not present in the current module. + * + * @param module target module + */ + void copy(ScriptModule module); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java new file mode 100644 index 0000000..45cc96c --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -0,0 +1,34 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Map; + +/** + * Manages all modules available to the script. + * The global module is always present and created automatically. + */ +public interface ScriptModuleManager { + + ScriptModuleExportResolverFactory getModuleExportResolver(); + + void setModuleExportResolver(ScriptModuleExportResolverFactory factory); + + /** + * Exports added here are available globally without import + */ + ScriptModule getGlobalModule(); + + @UnmodifiableView Map getModules(); + + default @Nullable ScriptModule getModule(String name) { + return this.getModules().get(name); + } + + void addModule(ScriptModule module); + + void removeModule(String name); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java new file mode 100644 index 0000000..26ae21e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java @@ -0,0 +1,8 @@ +package org.densy.scriptify.api.script.module.export; + +/** + * Represents any exportable element from a module. + */ +public interface ScriptExport { + String getName(); +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java new file mode 100644 index 0000000..dfc9ff2 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java @@ -0,0 +1,33 @@ +package org.densy.scriptify.api.script.module.export; + +import lombok.Getter; + +/** + * Universal wrapper for exporting Java values and classes. + * + *
+ *   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
+ * 
+ */ +@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; + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java new file mode 100644 index 0000000..6ca46b5 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java @@ -0,0 +1,7 @@ +package org.densy.scriptify.api.script.module.export.resolver; + +import org.densy.scriptify.api.script.module.export.ScriptExport; + +public interface ScriptModuleExportResolver { + Object resolve(ScriptExport export); +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java new file mode 100644 index 0000000..d11bfce --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java @@ -0,0 +1,7 @@ +package org.densy.scriptify.api.script.module.export.resolver; + +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; + +public interface ScriptModuleExportResolverFactory { + ScriptModuleExportResolver create(Object context) throws ScriptModuleWrongContextException; +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java new file mode 100644 index 0000000..970a3e1 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java @@ -0,0 +1,47 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleCopyException; +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.jetbrains.annotations.UnmodifiableView; + +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 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 void copy(ScriptModule module) { + // We need to verify that the module from which we want to copy exports + // does not contain any exports with the same name as in the current + // module but with a different hash. + boolean conflicts = module.getExports().stream().anyMatch(e -> + exports.values().stream().anyMatch(existing -> + existing.getName().equals(e.getName()) && + existing.hashCode() != e.hashCode() + ) + ); + + if (conflicts) { + throw new ScriptModuleCopyException("The copy operation cannot be performed: both modules contain different exports with the same name"); + } + + module.getExports().forEach(this::export); + } + + @Override + public @UnmodifiableView Collection getExports() { + return Collections.unmodifiableCollection(exports.values()); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java new file mode 100644 index 0000000..14546be --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java @@ -0,0 +1,11 @@ +package org.densy.scriptify.core.script.module; + +import org.jetbrains.annotations.NotNull; + +public final class ScriptGlobalModule extends AbstractScriptModule { + + @Override + public @NotNull String getName() { + return "global"; + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java new file mode 100644 index 0000000..b8bd872 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java @@ -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; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java new file mode 100644 index 0000000..9f0a759 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java @@ -0,0 +1,20 @@ +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(); + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java new file mode 100644 index 0000000..66c5c14 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java @@ -0,0 +1,20 @@ +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 ScriptFunctionDefinitionExport implements ScriptExport { + + private final ScriptFunctionDefinition definition; + + public ScriptFunctionDefinitionExport(ScriptFunctionDefinition definition) { + this.definition = definition; + } + + @Override + public String getName() { + return definition.getFunction().getName(); + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java new file mode 100644 index 0000000..7caf3ea --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java @@ -0,0 +1,20 @@ +package org.densy.scriptify.core.script.module.export; + +import lombok.Getter; +import org.densy.scriptify.api.script.function.ScriptFunction; +import org.densy.scriptify.api.script.module.export.ScriptExport; + +@Getter +public final class ScriptFunctionExport implements ScriptExport { + + private final ScriptFunction function; + + public ScriptFunctionExport(ScriptFunction function) { + this.function = function; + } + + @Override + public String getName() { + return function.getName(); + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java new file mode 100644 index 0000000..e869fc5 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java @@ -0,0 +1,26 @@ +package org.densy.scriptify.core.script.module.export.resolver; + +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public abstract class MappedModuleExportResolver implements ScriptModuleExportResolver { + + private final Map, Function> resolvers = new HashMap<>(); + + public void map(Class type, Function resolver) { + resolvers.put(type, export -> resolver.apply(type.cast(export))); + } + + @Override + public Object resolve(ScriptExport export) { + Function resolver = resolvers.get(export.getClass()); + if (resolver == null) { + throw new UnsupportedOperationException("No resolver found for export " + export.getClass()); + } + return resolver.apply(export); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java index 929f912..e504db2 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java @@ -1,6 +1,7 @@ package org.densy.scriptify.js.graalvm.script; import org.densy.scriptify.api.exception.ScriptException; +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; import org.densy.scriptify.api.script.CompiledScript; import org.densy.scriptify.api.script.Script; import org.densy.scriptify.api.script.ScriptObject; @@ -8,33 +9,49 @@ 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.module.export.resolver.ScriptModuleExportResolver; 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.ScriptFunctionDefinitionExport; 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; +import java.util.function.Supplier; public class JsScript implements Script { 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 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 @@ -59,6 +76,18 @@ public void addExtraScript(String script) { @Override public CompiledScript compile(String script) throws ScriptException { + // A context reference, so that once it has been created, + // we can access it in the file system + AtomicReference contextRef = new AtomicReference<>(); + + Supplier resolverSupplier = () -> { + try { + return moduleManager.getModuleExportResolver().create(contextRef.get()); + } catch (ScriptModuleWrongContextException e) { + throw new RuntimeException(e); + } + }; + Context.Builder builder = Context.newBuilder("js") .allowHostAccess(HostAccess.newBuilder(HostAccess.ALL) // Mapping for the ScriptObject class required @@ -69,6 +98,9 @@ public CompiledScript compile(String script) throws ScriptException { object -> true, ScriptObject::getValue ) + .build()) + .allowIO(IOAccess.newBuilder() + .fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get, resolverSupplier)) .build()); // If security mode is enabled, search all exclusions @@ -80,16 +112,15 @@ public CompiledScript 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 ScriptFunctionDefinitionExport(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(); @@ -99,7 +130,11 @@ public CompiledScript 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); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java new file mode 100644 index 0000000..912bea9 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -0,0 +1,79 @@ +package org.densy.scriptify.js.graalvm.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; +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.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; +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.densy.scriptify.js.graalvm.script.module.export.resolver.GraalModuleExportResolverFactory; +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 modules = new LinkedHashMap<>(); + private ScriptModuleExportResolverFactory moduleExportResolverFactory; + + public GraalModuleManager(Script script) { + this.script = script; + this.setModuleExportResolver(new GraalModuleExportResolverFactory(script)); + } + + @Override + public ScriptModuleExportResolverFactory getModuleExportResolver() { + return moduleExportResolverFactory; + } + + @Override + public void setModuleExportResolver(ScriptModuleExportResolverFactory moduleExportResolverFactory) { + this.moduleExportResolverFactory = Objects.requireNonNull(moduleExportResolverFactory, "moduleExportResolverFactory cannot be null"); + } + + @Override + public ScriptGlobalModule getGlobalModule() { + return globalModule; + } + + @Override + public Map getModules() { + return modules; + } + + @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"); + + try { + ScriptModuleExportResolver resolver = moduleExportResolverFactory.create(context); + for (ScriptExport export : globalModule.getExports()) { + bindings.putMember(export.getName(), resolver.resolve(export)); + } + } catch (ScriptModuleWrongContextException e) { + throw new RuntimeException(e); + } + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java new file mode 100644 index 0000000..6948d46 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java @@ -0,0 +1,23 @@ +package org.densy.scriptify.js.graalvm.script.module.export.resolver; + +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.core.script.module.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionDefinitionExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.core.script.module.export.resolver.MappedModuleExportResolver; +import org.densy.scriptify.js.graalvm.script.JsFunction; +import org.graalvm.polyglot.Context; + +public final class GraalModuleExportResolver extends MappedModuleExportResolver { + + public GraalModuleExportResolver(Script script, Context context) { + this.map(ScriptValueExport.class, export -> context.asValue(export.getValue())); + this.map(ScriptFunctionExport.class, export -> new JsFunction(script, script.getFunctionManager() + .getFunctionDefinitionFactory() + .create(export.getFunction()) + )); + this.map(ScriptFunctionDefinitionExport.class, export -> new JsFunction(script, export.getDefinition())); + this.map(ScriptConstantExport.class, export -> export.getConstant().getValue()); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java new file mode 100644 index 0000000..ea782df --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java @@ -0,0 +1,24 @@ +package org.densy.scriptify.js.graalvm.script.module.export.resolver; + +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; +import org.graalvm.polyglot.Context; + +public final class GraalModuleExportResolverFactory implements ScriptModuleExportResolverFactory { + + private final Script script; + + public GraalModuleExportResolverFactory(Script script) { + this.script = script; + } + + @Override + public ScriptModuleExportResolver create(Object context) throws ScriptModuleWrongContextException { + if (!(context instanceof Context graalContext)) { + throw new ScriptModuleWrongContextException(Context.class, context.getClass()); + } + return new GraalModuleExportResolver(script, graalContext); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java new file mode 100644 index 0000000..822854e --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -0,0 +1,145 @@ +package org.densy.scriptify.js.graalvm.script.module.fs; + +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.js.graalvm.script.module.fs.util.ByteArrayChannel; +import org.densy.scriptify.js.graalvm.script.module.fs.util.JsModuleSourceGenerator; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.io.FileSystem; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +public class VirtualModuleFileSystem implements FileSystem { + + private static final String SCHEME = "scriptify"; + + private final FileSystem real = FileSystem.newDefaultFileSystem(); + private final ScriptModuleManager moduleManager; + private final Supplier contextSupplier; + private final Supplier resolverSupplier; + + private final Map modulePathCache = new HashMap<>(); + + public VirtualModuleFileSystem( + ScriptModuleManager moduleManager, + Supplier contextSupplier, + Supplier resolverSupplier + ) { + this.moduleManager = moduleManager; + this.contextSupplier = contextSupplier; + this.resolverSupplier = resolverSupplier; + } + + @Override + public Path parsePath(String path) { + if (moduleManager.getModule(path) != null) { + return this.resolveVirtualPath(path); + } + return real.parsePath(path); + } + + @Override + public Path parsePath(URI uri) { + if (SCHEME.equals(uri.getScheme())) { + return this.resolveVirtualPath(uri.getHost()); + } + return real.parsePath(uri); + } + + @Override + public void checkAccess(Path path, Set modes, LinkOption... linkOptions) throws IOException { + if (this.isVirtual(path)) { + return; + } + real.checkAccess(path, modes, linkOptions); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + if (this.isVirtual(path)) { + Map attrs = new HashMap<>(); + attrs.put("isRegularFile", true); + attrs.put("isDirectory", false); + attrs.put("size", 0L); + attrs.put("lastModifiedTime", FileTime.fromMillis(0)); + attrs.put("creationTime", FileTime.fromMillis(0)); + attrs.put("lastAccessTime", FileTime.fromMillis(0)); + return attrs; + } + return real.readAttributes(path, attributes, options); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + if (this.isVirtual(path)) { + String moduleName = getModuleName(path); + ScriptModule module = moduleManager.getModule(moduleName); + if (module == null) { + throw new IOException("Scriptify module not found: " + moduleName); + } + byte[] source = JsModuleSourceGenerator + .generateModuleSource(contextSupplier.get(), module, resolverSupplier.get()) + .getBytes(StandardCharsets.UTF_8); + return new ByteArrayChannel(source); + } + return real.newByteChannel(path, options, attrs); + } + + @Override + public Path toAbsolutePath(Path path) { + if (isVirtual(path)) return path; + return real.toAbsolutePath(path); + } + + @Override + public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { + if (isVirtual(path)) return path; + return real.toRealPath(path, linkOptions); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + return real.newDirectoryStream(dir, filter); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + real.createDirectory(dir, attrs); + } + + @Override + public void delete(Path path) throws IOException { + real.delete(path); + } + + private Path resolveVirtualPath(String moduleName) { + return modulePathCache.computeIfAbsent(moduleName, name -> Paths.get( + System.getProperty("java.io.tmpdir"), + "scriptify", + JsModuleSourceGenerator.encodeModuleName(name) + ".mjs" + )); + } + + private boolean isVirtual(Path path) { + return modulePathCache.containsValue(path); + } + + private String getModuleName(Path path) { + return modulePathCache.entrySet().stream() + .filter(e -> e.getValue().equals(path)) + .map(Map.Entry::getKey) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Unknown virtual path: " + path)); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java new file mode 100644 index 0000000..f9a50fe --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java @@ -0,0 +1,60 @@ +package org.densy.scriptify.js.graalvm.script.module.fs.util; + +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +public class ByteArrayChannel implements SeekableByteChannel { + + private final byte[] data; + private int position = 0; + private boolean open = true; + + public ByteArrayChannel(byte[] data) { + this.data = data; + } + + @Override + public int read(ByteBuffer dst) { + if (position >= data.length) return -1; + int toRead = Math.min(dst.remaining(), data.length - position); + dst.put(data, position, toRead); + position += toRead; + return toRead; + } + + @Override + public SeekableByteChannel position(long pos) { + position = (int) pos; + return this; + } + + @Override + public long position() { + return position; + } + + @Override + public long size() { + return data.length; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() { + open = false; + } + + @Override + public int write(ByteBuffer src) { + throw new UnsupportedOperationException(); + } + + @Override + public SeekableByteChannel truncate(long size) { + throw new UnsupportedOperationException(); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java new file mode 100644 index 0000000..c273451 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java @@ -0,0 +1,57 @@ +package org.densy.scriptify.js.graalvm.script.module.fs.util; + +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.graalvm.polyglot.Context; + +import java.util.ArrayList; +import java.util.List; + +public final class JsModuleSourceGenerator { + + public static final String BRIDGE_PREFIX = "__scriptify_bridge_"; + + public static String generateModuleSource( + Context context, + ScriptModule module, + ScriptModuleExportResolver resolver + ) { + StringBuilder builder = new StringBuilder(); + builder.append("// @generated module: ").append(module.getName()).append("\n"); + + List names = new ArrayList<>(); + + for (ScriptExport export : module.getExports()) { + String name = export.getName(); + names.add(name); + + if (export instanceof ScriptValueExport valueExport && valueExport.isClass()) { + Class valueClass = (Class) valueExport.getValue(); + builder.append("const ").append(name) + .append(" = Java.type('").append(valueClass.getName()).append("');\n"); + } else { + Object resolved = resolver.resolve(export); + putBridge(context, builder, name, resolved); + } + } + + builder.append("\nexport { ").append(String.join(", ", names)).append(" };\n"); + return builder.toString(); + } + + private static void putBridge(Context context, StringBuilder sb, String name, Object value) { + String bridge = BRIDGE_PREFIX + name; + context.getBindings("js").putMember(bridge, value); + sb.append("const ").append(name).append(" = globalThis.").append(bridge).append(";\n"); + } + + public static String encodeModuleName(String name) { + return name.replace("@", "_at_").replace("/", "__"); + } + + public static String decodeModuleName(String encoded) { + return encoded.replace("_at_", "@").replace("__", "/"); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java index 81c2e48..1638a46 100644 --- a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java @@ -7,6 +7,7 @@ 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; @@ -30,6 +31,12 @@ public ScriptSecurityManager getSecurityManager() { return securityManager; } + @Override + public ScriptModuleManager getModuleManager() { + // TODO: implement + return null; + } + @Override public ScriptFunctionManager getFunctionManager() { return functionManager;