diff --git a/core/src/main/java/org/densy/scriptify/core/script/function/definition/ScriptFunctionArgumentDefinitionImpl.java b/core/src/main/java/org/densy/scriptify/core/script/function/definition/ScriptFunctionArgumentDefinitionImpl.java index 473f4f5..023dd4d 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/function/definition/ScriptFunctionArgumentDefinitionImpl.java +++ b/core/src/main/java/org/densy/scriptify/core/script/function/definition/ScriptFunctionArgumentDefinitionImpl.java @@ -1,9 +1,9 @@ package org.densy.scriptify.core.script.function.definition; -import org.densy.scriptify.api.script.function.definition.ScriptFunctionArgumentDefinition; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; +import org.densy.scriptify.api.script.function.definition.ScriptFunctionArgumentDefinition; @Getter @AllArgsConstructor 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 ae36861..b07cc09 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 @@ -11,7 +11,11 @@ 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.rhino.script.module.RhinoModuleManager; +import org.densy.scriptify.js.rhino.script.module.RhinoModuleSourceTransformer; import org.mozilla.javascript.Context; import org.mozilla.javascript.ScriptableObject; @@ -22,6 +26,7 @@ public class JsScript implements Script { private final ScriptSecurityManager securityManager = new StandardSecurityManager(); + private final RhinoModuleManager moduleManager = new RhinoModuleManager(this); private ScriptFunctionManager functionManager = new StandardFunctionManager(); private ScriptConstantManager constantManager = new StandardConstantManager(); private final List extraScript = new ArrayList<>(); @@ -33,7 +38,7 @@ public ScriptSecurityManager getSecurityManager() { @Override public ScriptModuleManager getModuleManager() { - throw new UnsupportedOperationException("Rhino does not support a module system."); + return moduleManager; } @Override @@ -65,7 +70,8 @@ public void addExtraScript(String script) { public CompiledScript compile(String script) throws ScriptException { try { Context context = Context.enter(); - context.setWrapFactory(new JsWrapFactory()); + context.setLanguageVersion(Context.VERSION_ES6); + context.setWrapFactory(new JsWrapFactory(moduleManager.getScriptAccess())); ScriptableObject scope = context.initStandardObjects(); @@ -76,12 +82,13 @@ public CompiledScript compile(String script) throws ScriptException { } for (ScriptFunctionDefinition definition : functionManager.getFunctions().values()) { - scope.put(definition.getFunction().getName(), scope, new JsFunction(this, definition)); + moduleManager.getGlobalModule().export(new ScriptFunctionDefinitionExport(definition)); } for (ScriptConstant constant : constantManager.getConstants().values()) { - ScriptableObject.putConstProperty(scope, constant.getName(), constant.getValue()); + moduleManager.getGlobalModule().export(new ScriptConstantExport(constant)); } + moduleManager.applyTo(context, scope); // Building full script including extra script code StringBuilder fullScript = new StringBuilder(); @@ -90,7 +97,12 @@ public CompiledScript compile(String script) throws ScriptException { } fullScript.append(script); - var compiled = context.compileString(fullScript.toString(), "script", 1, null); + var compiled = context.compileString( + RhinoModuleSourceTransformer.transformScript(fullScript.toString()), + "script", + 1, + null + ); return new JsCompiledScript(context, scope, compiled); } catch (Exception e) { throw new ScriptException(e); diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsWrapFactory.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsWrapFactory.java index a5f10cd..bd66d6e 100644 --- a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsWrapFactory.java +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsWrapFactory.java @@ -1,13 +1,20 @@ package org.densy.scriptify.js.rhino.script; import org.densy.scriptify.api.script.ScriptObject; +import org.densy.scriptify.api.script.module.export.access.ScriptAccess; +import org.densy.scriptify.js.rhino.script.access.RestrictedNativeJavaClass; +import org.densy.scriptify.js.rhino.script.access.RestrictedNativeJavaObject; +import org.densy.scriptify.js.rhino.script.access.RhinoScriptAccessSupport; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.WrapFactory; public class JsWrapFactory extends WrapFactory { - public JsWrapFactory() { + private final ScriptAccess scriptAccess; + + public JsWrapFactory(ScriptAccess scriptAccess) { + this.scriptAccess = scriptAccess; this.setJavaPrimitiveWrap(false); } @@ -15,8 +22,24 @@ public JsWrapFactory() { public Object wrap(Context context, Scriptable scope, Object object, Class staticType) { // Convert the ScriptObject class to the value it contains if (object instanceof ScriptObject scriptObject) { - return Context.javaToJS(scriptObject.getValue(), scope); + return super.wrap(context, scope, scriptObject.getValue(), staticType); } return super.wrap(context, scope, object, staticType); } + + @Override + public Scriptable wrapAsJavaObject(Context context, Scriptable scope, Object javaObject, Class staticType) { + if (RhinoScriptAccessSupport.isExplicit(scriptAccess)) { + return new RestrictedNativeJavaObject(scope, javaObject, staticType); + } + return super.wrapAsJavaObject(context, scope, javaObject, staticType); + } + + @Override + public Scriptable wrapJavaClass(Context context, Scriptable scope, Class javaClass) { + if (RhinoScriptAccessSupport.isExplicit(scriptAccess)) { + return new RestrictedNativeJavaClass(scope, javaClass); + } + return super.wrapJavaClass(context, scope, javaClass); + } } diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedJavaMethodFunction.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedJavaMethodFunction.java new file mode 100644 index 0000000..37f19c8 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedJavaMethodFunction.java @@ -0,0 +1,62 @@ +package org.densy.scriptify.js.rhino.script.access; + +import org.mozilla.javascript.BaseFunction; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +final class RestrictedJavaMethodFunction extends BaseFunction { + + private final Object target; + private final Class type; + private final String name; + private final boolean staticOnly; + + RestrictedJavaMethodFunction(Scriptable scope, Object target, Class type, String name, boolean staticOnly) { + this.target = target; + this.type = type; + this.name = name; + this.staticOnly = staticOnly; + this.setParentScope(scope); + this.setPrototype(getFunctionPrototype(scope)); + } + + @Override + public Object call(Context context, Scriptable scope, Scriptable thisObj, Object[] args) { + for (Method method : type.getMethods()) { + if (!isCandidate(method)) { + continue; + } + + Object[] converted = RhinoScriptAccessSupport.convertArguments( + context, + args, + method.getParameterTypes(), + method.isVarArgs() + ); + if (converted == null) { + continue; + } + + try { + Object result = method.invoke(RhinoScriptAccessSupport.isStatic(method) ? null : target, converted); + return context.getWrapFactory().wrap(context, scope, result, method.getReturnType()); + } catch (IllegalArgumentException ignored) { + // Try the next overload. + } catch (IllegalAccessException | InvocationTargetException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + throw Context.reportRuntimeError("No exported method '" + name + "' matches provided arguments"); + } + + private boolean isCandidate(Method method) { + return method.getName().equals(name) + && RhinoScriptAccessSupport.isExported(method) + && (!staticOnly || RhinoScriptAccessSupport.isStatic(method)) + && (staticOnly || !RhinoScriptAccessSupport.isStatic(method)); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedNativeJavaClass.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedNativeJavaClass.java new file mode 100644 index 0000000..5d62e79 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedNativeJavaClass.java @@ -0,0 +1,91 @@ +package org.densy.scriptify.js.rhino.script.access; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.NativeJavaClass; +import org.mozilla.javascript.Scriptable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +public final class RestrictedNativeJavaClass extends NativeJavaClass { + + public RestrictedNativeJavaClass(Scriptable scope, Class javaClass) { + super(scope, javaClass); + } + + @Override + public boolean has(String name, Scriptable start) { + Class type = getClassObject(); + return RhinoScriptAccessSupport.findExportedField(type, name, true) != null + || RhinoScriptAccessSupport.hasExportedMethod(type, name, true); + } + + @Override + public Object get(String name, Scriptable start) { + Class type = getClassObject(); + Field field = RhinoScriptAccessSupport.findExportedField(type, name, true); + if (field != null) { + try { + return Context.getCurrentContext().getWrapFactory().wrap( + Context.getCurrentContext(), + getParentScope(), + field.get(null), + field.getType() + ); + } catch (IllegalAccessException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + if (RhinoScriptAccessSupport.hasExportedMethod(type, name, true)) { + return new RestrictedJavaMethodFunction(getParentScope(), null, type, name, true); + } + + return Scriptable.NOT_FOUND; + } + + @Override + public void put(String name, Scriptable start, Object value) { + Field field = RhinoScriptAccessSupport.findExportedField(getClassObject(), name, true); + if (field == null) { + throw Context.reportRuntimeError("Java static member is not exported: " + name); + } + try { + field.set(null, Context.jsToJava(value, field.getType())); + } catch (IllegalAccessException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + @Override + public Object[] getIds() { + return RhinoScriptAccessSupport.getExportedClassMemberNames(getClassObject()).toArray(); + } + + @Override + public Scriptable construct(Context context, Scriptable scope, Object[] args) { + for (Constructor constructor : RhinoScriptAccessSupport.getExportedConstructors(getClassObject())) { + Object[] converted = RhinoScriptAccessSupport.convertArguments( + context, + args, + constructor.getParameterTypes(), + constructor.isVarArgs() + ); + if (converted == null) { + continue; + } + + try { + Object instance = constructor.newInstance(converted); + return context.getWrapFactory().wrapNewObject(context, scope, instance); + } catch (IllegalArgumentException ignored) { + // Try the next overload. + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + throw Context.reportRuntimeError("No exported constructor matches provided arguments for " + getClassObject().getName()); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedNativeJavaObject.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedNativeJavaObject.java new file mode 100644 index 0000000..221a838 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RestrictedNativeJavaObject.java @@ -0,0 +1,72 @@ +package org.densy.scriptify.js.rhino.script.access; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.Scriptable; + +import java.lang.reflect.Field; + +public final class RestrictedNativeJavaObject extends NativeJavaObject { + + public RestrictedNativeJavaObject(Scriptable scope, Object javaObject, Class staticType) { + super(scope, javaObject, staticType); + } + + @Override + public boolean has(String name, Scriptable start) { + Class type = getWrappedType(); + return RhinoScriptAccessSupport.findExportedField(type, name, false) != null + || RhinoScriptAccessSupport.hasExportedMethod(type, name, false); + } + + @Override + public Object get(String name, Scriptable start) { + Class type = getWrappedType(); + Field field = RhinoScriptAccessSupport.findExportedField(type, name, false); + if (field != null) { + try { + Object value = field.get(unwrap()); + return Context.getCurrentContext().getWrapFactory().wrap( + Context.getCurrentContext(), + getParentScope(), + value, + field.getType() + ); + } catch (IllegalAccessException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + if (RhinoScriptAccessSupport.hasExportedMethod(type, name, false)) { + return new RestrictedJavaMethodFunction(getParentScope(), unwrap(), type, name, false); + } + + return Scriptable.NOT_FOUND; + } + + @Override + public void put(String name, Scriptable start, Object value) { + Field field = RhinoScriptAccessSupport.findExportedField(getWrappedType(), name, false); + if (field == null) { + throw Context.reportRuntimeError("Java member is not exported: " + name); + } + try { + field.set(unwrap(), Context.jsToJava(value, field.getType())); + } catch (IllegalAccessException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + @Override + public Object[] getIds() { + return RhinoScriptAccessSupport.getExportedInstanceMemberNames(getWrappedType()).toArray(); + } + + private Class getWrappedType() { + Object wrapped = unwrap(); + if (staticType != null && staticType != Object.class) { + return staticType; + } + return wrapped.getClass(); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RhinoScriptAccessSupport.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RhinoScriptAccessSupport.java new file mode 100644 index 0000000..ad8539e --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/access/RhinoScriptAccessSupport.java @@ -0,0 +1,121 @@ +package org.densy.scriptify.js.rhino.script.access; + +import org.densy.scriptify.api.script.module.export.access.ScriptAccess; +import org.mozilla.javascript.Context; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +public final class RhinoScriptAccessSupport { + + private RhinoScriptAccessSupport() {} + + public static boolean isExplicit(ScriptAccess access) { + return access == ScriptAccess.EXPLICIT; + } + + static boolean isExported(AccessibleObject object) { + return object.isAnnotationPresent(ScriptAccess.Export.class); + } + + static Set getExportedInstanceMemberNames(Class type) { + Set names = new LinkedHashSet<>(); + for (Field field : type.getFields()) { + if (isExported(field) && !Modifier.isStatic(field.getModifiers())) { + names.add(field.getName()); + } + } + for (Method method : type.getMethods()) { + if (isExported(method) && !Modifier.isStatic(method.getModifiers())) { + names.add(method.getName()); + } + } + return names; + } + + static Set getExportedClassMemberNames(Class type) { + Set names = new LinkedHashSet<>(); + for (Field field : type.getFields()) { + if (isExported(field) && Modifier.isStatic(field.getModifiers())) { + names.add(field.getName()); + } + } + for (Method method : type.getMethods()) { + if (isExported(method) && Modifier.isStatic(method.getModifiers())) { + names.add(method.getName()); + } + } + return names; + } + + static Field findExportedField(Class type, String name, boolean staticOnly) { + for (Field field : type.getFields()) { + if (field.getName().equals(name) + && isExported(field) + && (!staticOnly || Modifier.isStatic(field.getModifiers()))) { + return field; + } + } + return null; + } + + static boolean hasExportedMethod(Class type, String name, boolean staticOnly) { + return Arrays.stream(type.getMethods()) + .anyMatch(method -> method.getName().equals(name) + && isExported(method) + && (!staticOnly || Modifier.isStatic(method.getModifiers())) + && (staticOnly || !Modifier.isStatic(method.getModifiers()))); + } + + static Constructor[] getExportedConstructors(Class type) { + return Arrays.stream(type.getConstructors()) + .filter(RhinoScriptAccessSupport::isExported) + .toArray(Constructor[]::new); + } + + static Object[] convertArguments(Context context, Object[] args, Class[] parameterTypes, boolean varArgs) { + if (!varArgs) { + if (args.length != parameterTypes.length) { + return null; + } + Object[] converted = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + converted[i] = convertArgument(context, args[i], parameterTypes[i]); + } + return converted; + } + + int fixedCount = parameterTypes.length - 1; + if (args.length < fixedCount) { + return null; + } + + Object[] converted = new Object[parameterTypes.length]; + for (int i = 0; i < fixedCount; i++) { + converted[i] = convertArgument(context, args[i], parameterTypes[i]); + } + + Class componentType = parameterTypes[fixedCount].getComponentType(); + Object varArgArray = java.lang.reflect.Array.newInstance(componentType, args.length - fixedCount); + for (int i = fixedCount; i < args.length; i++) { + java.lang.reflect.Array.set(varArgArray, i - fixedCount, convertArgument(context, args[i], componentType)); + } + converted[fixedCount] = varArgArray; + return converted; + } + + static boolean isStatic(Member member) { + return Modifier.isStatic(member.getModifiers()); + } + + private static Object convertArgument(Context context, Object value, Class targetType) { + return Context.jsToJava(value, targetType); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleContext.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleContext.java new file mode 100644 index 0000000..d3167f2 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleContext.java @@ -0,0 +1,7 @@ +package org.densy.scriptify.js.rhino.script.module; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ScriptableObject; + +public record RhinoModuleContext(Context context, ScriptableObject scope) { +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleLoader.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleLoader.java new file mode 100644 index 0000000..1fcd2c6 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleLoader.java @@ -0,0 +1,104 @@ +package org.densy.scriptify.js.rhino.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; +import org.densy.scriptify.api.script.module.ScriptExternalModule; +import org.densy.scriptify.api.script.module.ScriptInternalModule; +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.resolver.ScriptModuleExportResolver; +import org.mozilla.javascript.BaseFunction; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public final class RhinoModuleLoader extends BaseFunction { + + private final RhinoModuleManager moduleManager; + private final Context context; + private final ScriptableObject scope; + private final ScriptModuleExportResolver resolver; + private final Map moduleCache = new HashMap<>(); + + public RhinoModuleLoader(RhinoModuleManager moduleManager, Context context, ScriptableObject scope) { + this.moduleManager = moduleManager; + this.context = context; + this.scope = scope; + try { + this.resolver = moduleManager.getModuleExportResolver().create(new RhinoModuleContext(context, scope)); + } catch (ScriptModuleWrongContextException e) { + throw new IllegalStateException(e); + } + } + + @Override + public Object call(Context context, Scriptable scope, Scriptable thisObj, Object[] args) { + if (args.length == 0) { + throw Context.reportRuntimeError("Module name is required"); + } + return load(Context.toString(args[0])); + } + + public ScriptableObject load(String moduleName) { + ScriptableObject cached = moduleCache.get(moduleName); + if (cached != null) { + return cached; + } + + ScriptModule module = moduleManager.getModule(moduleName); + if (module instanceof ScriptInternalModule internalModule) { + return loadInternal(moduleName, internalModule); + } + if (module instanceof ScriptExternalModule externalModule) { + return loadExternal(moduleName, externalModule); + } + throw Context.reportRuntimeError("Script module not found: " + moduleName); + } + + private ScriptableObject loadInternal(String moduleName, ScriptInternalModule module) { + ScriptableObject moduleObject = createModuleObject(); + moduleCache.put(moduleName, moduleObject); + + for (ScriptExport export : module.getExports()) { + ScriptableObject.putProperty(moduleObject, export.getName(), resolver.resolve(export)); + } + + return moduleObject; + } + + private ScriptableObject loadExternal(String moduleName, ScriptExternalModule module) { + ScriptableObject exports = createModuleObject(); + moduleCache.put(moduleName, exports); + + try { + String source = new String(module.load(), StandardCharsets.UTF_8); + String transformed = RhinoModuleSourceTransformer.transformModule(source); + Object wrapper = context.evaluateString( + scope, + "(function(exports, __scriptify_require) {\n" + transformed + "\n})", + module.getSourceName(), + 1, + null + ); + if (!(wrapper instanceof Function function)) { + throw Context.reportRuntimeError("External module did not compile to a function: " + moduleName); + } + function.call(context, scope, scope, new Object[]{exports, this}); + return exports; + } catch (ScriptModuleLoadException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + private ScriptableObject createModuleObject() { + ScriptableObject object = (ScriptableObject) context.newObject(scope); + object.setPrototype(ScriptableObject.getObjectPrototype(scope)); + object.setParentScope(scope); + return object; + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleManager.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleManager.java new file mode 100644 index 0000000..afd76ab --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleManager.java @@ -0,0 +1,89 @@ +package org.densy.scriptify.js.rhino.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.access.ScriptAccess; +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.ScriptInternalGlobalModule; +import org.densy.scriptify.js.rhino.script.module.export.resolver.RhinoModuleExportResolverFactory; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ScriptableObject; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public class RhinoModuleManager implements ScriptModuleManager { + + private static final String REQUIRE_NAME = "__scriptify_require"; + + private final ScriptInternalGlobalModule globalModule = new ScriptInternalGlobalModule(); + private final Map modules = new LinkedHashMap<>(); + private ScriptModuleExportResolverFactory moduleExportResolverFactory; + private ScriptAccess scriptAccess = ScriptAccess.ALL; + + public RhinoModuleManager(Script script) { + this.setModuleExportResolver(new RhinoModuleExportResolverFactory(script)); + } + + @Override + public ScriptModuleExportResolverFactory getModuleExportResolver() { + return moduleExportResolverFactory; + } + + @Override + public void setModuleExportResolver(ScriptModuleExportResolverFactory moduleExportResolverFactory) { + this.moduleExportResolverFactory = Objects.requireNonNull(moduleExportResolverFactory, "moduleExportResolverFactory cannot be null"); + } + + @Override + public ScriptAccess getScriptAccess() { + return scriptAccess; + } + + @Override + public void setScriptAccess(ScriptAccess scriptAccess) { + this.scriptAccess = Objects.requireNonNull(scriptAccess, "scriptAccess cannot be null"); + } + + @Override + public ScriptInternalGlobalModule getGlobalModule() { + return globalModule; + } + + @Override + public Map getModules() { + return Collections.unmodifiableMap(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, ScriptableObject scope) { + RhinoModuleLoader loader = new RhinoModuleLoader(this, context, scope); + ScriptableObject.putProperty(scope, REQUIRE_NAME, loader); + + try { + ScriptModuleExportResolver resolver = moduleExportResolverFactory.create(new RhinoModuleContext(context, scope)); + for (ScriptExport export : globalModule.getExports()) { + ScriptableObject.putProperty(scope, export.getName(), resolver.resolve(export)); + } + } catch (ScriptModuleWrongContextException e) { + throw new RuntimeException(e); + } + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleSourceTransformer.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleSourceTransformer.java new file mode 100644 index 0000000..f34da4e --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/RhinoModuleSourceTransformer.java @@ -0,0 +1,146 @@ +package org.densy.scriptify.js.rhino.script.module; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class RhinoModuleSourceTransformer { + + private static final String IDENTIFIER = "[A-Za-z_$][A-Za-z0-9_$]*"; + + private static final Pattern IMPORT_STAR = Pattern.compile( + "(?m)^\\s*import\\s+\\*\\s+as\\s+(" + IDENTIFIER + ")\\s+from\\s+['\"]([^'\"]+)['\"]\\s*;?\\s*$" + ); + private static final Pattern IMPORT_NAMED = Pattern.compile( + "(?m)^\\s*import\\s+\\{([^}]+)}\\s+from\\s+['\"]([^'\"]+)['\"]\\s*;?\\s*$" + ); + private static final Pattern IMPORT_DEFAULT = Pattern.compile( + "(?m)^\\s*import\\s+(" + IDENTIFIER + ")\\s+from\\s+['\"]([^'\"]+)['\"]\\s*;?\\s*$" + ); + private static final Pattern IMPORT_SIDE_EFFECT = Pattern.compile( + "(?m)^\\s*import\\s+['\"]([^'\"]+)['\"]\\s*;?\\s*$" + ); + private static final Pattern EXPORT_NAMED = Pattern.compile( + "(?m)^\\s*export\\s*\\{([^}]+)}\\s*;?\\s*$" + ); + private static final Pattern EXPORT_DECLARATION = Pattern.compile( + "(?m)^\\s*export\\s+(const|let|var|function|class)\\s+(" + IDENTIFIER + ")" + ); + private static final Pattern EXPORT_DEFAULT = Pattern.compile("\\bexport\\s+default\\s+"); + + private RhinoModuleSourceTransformer() {} + + public static String transformScript(String source) { + return transformImports(source); + } + + public static String transformModule(String source) { + List declaredExports = new ArrayList<>(); + String transformed = transformImports(source); + + var declarationMatcher = EXPORT_DECLARATION.matcher(transformed); + var declarationBuffer = new StringBuilder(); + while (declarationMatcher.find()) { + declaredExports.add(declarationMatcher.group(2)); + declarationMatcher.appendReplacement( + declarationBuffer, + Matcher.quoteReplacement(declarationMatcher.group(1) + " " + declarationMatcher.group(2)) + ); + } + declarationMatcher.appendTail(declarationBuffer); + transformed = declarationBuffer.toString(); + + var namedMatcher = EXPORT_NAMED.matcher(transformed); + var namedBuffer = new StringBuilder(); + while (namedMatcher.find()) { + namedMatcher.appendReplacement( + namedBuffer, + Matcher.quoteReplacement(toExportAssignments(namedMatcher.group(1))) + ); + } + namedMatcher.appendTail(namedBuffer); + transformed = namedBuffer.toString(); + + transformed = EXPORT_DEFAULT.matcher(transformed).replaceAll("exports.default = "); + + if (!declaredExports.isEmpty()) { + StringBuilder builder = new StringBuilder(transformed); + builder.append("\n"); + for (String export : declaredExports) { + builder.append("exports.").append(export).append(" = ").append(export).append(";\n"); + } + transformed = builder.toString(); + } + + return transformed; + } + + private static String transformImports(String source) { + String transformed = replaceAll(IMPORT_STAR, source, matcher -> + "const " + matcher.group(1) + " = __scriptify_require(\"" + escape(matcher.group(2)) + "\");" + ); + transformed = replaceAll(IMPORT_NAMED, transformed, matcher -> + "const { " + toDestructuringSpec(matcher.group(1)) + " } = __scriptify_require(\"" + escape(matcher.group(2)) + "\");" + ); + transformed = replaceAll(IMPORT_DEFAULT, transformed, matcher -> + "const " + matcher.group(1) + " = __scriptify_require(\"" + escape(matcher.group(2)) + "\").default;" + ); + return replaceAll(IMPORT_SIDE_EFFECT, transformed, matcher -> + "__scriptify_require(\"" + escape(matcher.group(1)) + "\");" + ); + } + + private static String toDestructuringSpec(String spec) { + List parts = new ArrayList<>(); + for (String item : spec.split(",")) { + String trimmed = item.trim(); + if (trimmed.isEmpty()) { + continue; + } + String[] alias = trimmed.split("\\s+as\\s+"); + if (alias.length == 2) { + parts.add(alias[0].trim() + ": " + alias[1].trim()); + } else { + parts.add(trimmed); + } + } + return String.join(", ", parts); + } + + private static String toExportAssignments(String spec) { + StringBuilder builder = new StringBuilder(); + for (String item : spec.split(",")) { + String trimmed = item.trim(); + if (trimmed.isEmpty()) { + continue; + } + String[] alias = trimmed.split("\\s+as\\s+"); + if (alias.length == 2) { + builder.append("exports.") + .append(alias[1].trim()) + .append(" = ") + .append(alias[0].trim()) + .append(";\n"); + } else { + builder.append("exports.").append(trimmed).append(" = ").append(trimmed).append(";\n"); + } + } + return builder.toString(); + } + + private static String replaceAll(Pattern pattern, String source, Function replacement) { + Matcher matcher = pattern.matcher(source); + StringBuilder buffer = new StringBuilder(); + while (matcher.find()) { + matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement.apply(matcher))); + } + matcher.appendTail(buffer); + return buffer.toString(); + } + + private static String escape(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/export/resolver/RhinoModuleExportResolver.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/export/resolver/RhinoModuleExportResolver.java new file mode 100644 index 0000000..d5cc0a3 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/export/resolver/RhinoModuleExportResolver.java @@ -0,0 +1,34 @@ +package org.densy.scriptify.js.rhino.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.rhino.script.JsFunction; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ScriptableObject; + +public final class RhinoModuleExportResolver extends MappedModuleExportResolver { + + public RhinoModuleExportResolver(Script script, Context context, ScriptableObject scope) { + this.mapping(ScriptValueExport.class, export -> { + if (export.isClass()) { + return context.getWrapFactory().wrapJavaClass(context, scope, (Class) export.getValue()); + } + return context.getWrapFactory().wrap(context, scope, export.getValue(), Object.class); + }); + this.mapping(ScriptFunctionExport.class, export -> new JsFunction(script, script.getFunctionManager() + .getFunctionDefinitionFactory() + .create(export.getFunction()) + )); + this.mapping(ScriptFunctionDefinitionExport.class, export -> new JsFunction(script, export.getDefinition())); + this.mapping(ScriptConstantExport.class, export -> context.getWrapFactory().wrap( + context, + scope, + export.getConstant().getValue(), + Object.class + )); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/export/resolver/RhinoModuleExportResolverFactory.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/export/resolver/RhinoModuleExportResolverFactory.java new file mode 100644 index 0000000..94e1866 --- /dev/null +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/module/export/resolver/RhinoModuleExportResolverFactory.java @@ -0,0 +1,24 @@ +package org.densy.scriptify.js.rhino.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.densy.scriptify.js.rhino.script.module.RhinoModuleContext; + +public final class RhinoModuleExportResolverFactory implements ScriptModuleExportResolverFactory { + + private final Script script; + + public RhinoModuleExportResolverFactory(Script script) { + this.script = script; + } + + @Override + public ScriptModuleExportResolver create(Object context) throws ScriptModuleWrongContextException { + if (!(context instanceof RhinoModuleContext rhinoContext)) { + throw new ScriptModuleWrongContextException(RhinoModuleContext.class, context.getClass()); + } + return new RhinoModuleExportResolver(script, rhinoContext.context(), rhinoContext.scope()); + } +}