From 865afa892e6ae9c4f3a5eee3c1863a63a63e4ae2 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sun, 26 Apr 2026 14:06:01 +0200 Subject: [PATCH 1/4] feat: start work on modules declaration generating --- build.gradle.kts | 7 +- .../ScriptTsClassDeclarationGenerator.java | 116 ++++++++++++++++++ .../ScriptTsDeclarationGenerator.java | 93 +++----------- .../declaration/ScriptTsProjectGenerator.java | 42 +++++-- .../org/densy/scriptify/declaration/Test.java | 49 ++++++++ .../declaration/util/TsImportUtil.java | 30 +++++ .../ScriptTsExportDeclarationWriter.java | 114 +++++++++++++++++ .../ScriptTsModuleDeclarationWriter.java | 52 ++++++++ 8 files changed, 414 insertions(+), 89 deletions(-) create mode 100644 src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java create mode 100644 src/main/java/org/densy/scriptify/declaration/Test.java create mode 100644 src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java create mode 100644 src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java create mode 100644 src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java diff --git a/build.gradle.kts b/build.gradle.kts index cfb47f2..32877c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ version = "1.0.1-SNAPSHOT" java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } @@ -19,7 +19,10 @@ repositories { } dependencies { - api("org.densy.scriptify:api:1.5.0-SNAPSHOT") + api("org.densy.scriptify:api:1.6.0-beta") + api("org.densy.scriptify:core:1.6.0-beta") + api("org.densy.scriptify:script-js-graalvm:1.6.0-beta") + api("org.densy.scriptify:common:1.6.0-beta") compileOnlyApi("org.projectlombok:lombok:1.18.36") annotationProcessor("org.projectlombok:lombok:1.18.36") } diff --git a/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java b/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java new file mode 100644 index 0000000..99f95c3 --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java @@ -0,0 +1,116 @@ +package org.densy.scriptify.declaration; + +import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.List; + +public class ScriptTsClassDeclarationGenerator { + + public String generate(Class clazz, String indent, String prefix) { + if (clazz.isEnum()) { + return generateEnum(clazz, indent, prefix); + } + if (clazz.isInterface()) { + return generateInterface(clazz, indent, prefix); + } + return generateClass(clazz, indent, prefix); + } + + private String generateClass(Class clazz, String indent, String prefix) { + StringBuilder builder = new StringBuilder(); + builder.append(indent).append(prefix).append("class ") + .append(clazz.getSimpleName()).append(" {\n"); + + // constructors + for (Constructor constructor : clazz.getConstructors()) { + builder.append(indent).append(" constructor("); + builder.append(generateParams(constructor.getGenericParameterTypes(), + constructor.getParameters())); + builder.append(");\n"); + } + + // public fields + for (Field field : clazz.getFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + builder.append(indent).append(" "); + if (Modifier.isFinal(field.getModifiers())) builder.append("readonly "); + builder.append(field.getName()).append(": ") + .append(JavaToTypeScriptConverter.convert(field.getGenericType())) + .append(";\n"); + } + + // public methods + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == Object.class) continue; + if (Modifier.isStatic(method.getModifiers())) continue; + + builder.append(indent).append(" ") + .append(method.getName()).append("(") + .append(generateParams(method.getGenericParameterTypes(), method.getParameters())) + .append("): ") + .append(JavaToTypeScriptConverter.convert(method.getGenericReturnType())) + .append(";\n"); + } + + builder.append(indent).append("}"); + return builder.toString(); + } + + private String generateInterface(Class clazz, String indent, String prefix) { + StringBuilder builder = new StringBuilder(); + builder.append(indent).append(prefix).append("interface ") + .append(clazz.getSimpleName()).append(" {\n"); + + // interface constants and static fields + for (Field field : clazz.getFields()) { + builder.append(indent).append(" readonly ") + .append(field.getName()).append(": ") + .append(JavaToTypeScriptConverter.convert(field.getGenericType())) + .append(";\n"); + } + + // interface methods + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == Object.class) continue; + + builder.append(indent).append(" ") + .append(method.getName()).append("(") + .append(generateParams(method.getGenericParameterTypes(), method.getParameters())) + .append("): ") + .append(JavaToTypeScriptConverter.convert(method.getGenericReturnType())) + .append(";\n"); + } + + builder.append(indent).append("}"); + return builder.toString(); + } + + private String generateEnum(Class clazz, String indent, String prefix) { + StringBuilder builder = new StringBuilder(); + builder.append(indent).append(prefix).append("enum ") + .append(clazz.getSimpleName()).append(" {\n"); + + Object[] constants = clazz.getEnumConstants(); + for (int i = 0; i < constants.length; i++) { + builder.append(indent).append(" ").append(((Enum) constants[i]).name()); + if (i < constants.length - 1) builder.append(","); + builder.append("\n"); + } + + builder.append(indent).append("}"); + return builder.toString(); + } + + private String generateParams(Type[] types, Parameter[] parameters) { + List params = new ArrayList<>(); + for (int i = 0; i < types.length; i++) { + String name = parameters[i].isNamePresent() + ? parameters[i].getName() + : "arg" + i; + params.add(name + ": " + JavaToTypeScriptConverter.convert(types[i])); + } + return String.join(", ", params); + } +} \ No newline at end of file diff --git a/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java b/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java index bbce27e..d7d8120 100644 --- a/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java @@ -1,37 +1,31 @@ package org.densy.scriptify.declaration; import org.densy.scriptify.api.script.Script; -import org.densy.scriptify.api.script.constant.ScriptConstant; -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.function.definition.ScriptFunctionExecutor; -import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter; +import org.densy.scriptify.api.script.module.ScriptInternalModule; +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.declaration.writer.ScriptTsExportDeclarationWriter; +import org.densy.scriptify.declaration.writer.ScriptTsModuleDeclarationWriter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -/** - * Generator for Scriptify functions declarations. - */ public class ScriptTsDeclarationGenerator { private final Script script; + private final ScriptTsModuleDeclarationWriter moduleWriter; public ScriptTsDeclarationGenerator(Script script) { this.script = script; + ScriptTsClassDeclarationGenerator classGenerator = new ScriptTsClassDeclarationGenerator(); + ScriptTsExportDeclarationWriter exportWriter = new ScriptTsExportDeclarationWriter(script, classGenerator); + this.moduleWriter = new ScriptTsModuleDeclarationWriter(exportWriter); + } - /** - * Returns the main header of the declaration. - * - * @return String header - */ protected String getHeader() { try (InputStream is = getClass().getClassLoader().getResourceAsStream("header.txt")) { if (is == null) { @@ -43,75 +37,26 @@ protected String getHeader() { } } - - /** - * Generates a declaration of constants and functions. - * - * @return Generated declaration - */ public String generate() { - StringBuilder sb = new StringBuilder(this.getHeader()); - - // Generate constant declarations - ScriptConstantManager constantManager = script.getConstantManager(); - if (constantManager != null) { - for (ScriptConstant constant : constantManager.getConstants().values()) { - Object value = constant.getValue(); - sb.append("declare const ") - .append(constant.getName()) - .append(": ") - .append(JavaToTypeScriptConverter.convert(value != null ? value.getClass() : null)) - .append(";\n\n"); - } - } - - // Generate function declarations - ScriptFunctionManager functionManager = script.getFunctionManager(); - if (functionManager != null) { - for (ScriptFunctionDefinition def : functionManager.getFunctions().values()) { - for (ScriptFunctionExecutor executor : def.getExecutors()) { - sb.append("/**\n") - .append(" * Script ").append(def.getFunction().getName()).append(" function").append("\n"); - for (var arg : executor.getArguments()) { - sb.append(" * @param ").append(arg.getName()) - .append(" ").append(arg.isRequired() ? "(required)" : "(optional)") - .append("\n"); - } - sb.append(" */\n"); + StringBuilder builder = new StringBuilder(getHeader()); + ScriptModuleManager moduleManager = script.getModuleManager(); - sb.append("declare function ") - .append(def.getFunction().getName()) - .append("("); + moduleWriter.writeGlobal(builder, moduleManager.getGlobalModule()); - List params = new ArrayList<>(); - for (var arg : executor.getArguments()) { - params.add( - arg.getName() + - (arg.isRequired() ? "" : "?") + - ": " + JavaToTypeScriptConverter.convert(arg.getType()) - ); - } - sb.append(String.join(", ", params)) - .append("): ") - .append(JavaToTypeScriptConverter.convert(executor.getMethod().getReturnType())) - .append(";\n\n"); - } + for (ScriptModule module : moduleManager.getModules().values()) { + if (module instanceof ScriptInternalModule internalModule) { + moduleWriter.writeNamed(builder, internalModule); } } - return sb.toString(); + return builder.toString(); } - /** - * Generates and saves the declaration as a file. - * - * @param path Save path - */ public void save(Path path) { try { - Files.writeString(path, this.generate(), StandardCharsets.UTF_8); + Files.writeString(path, generate(), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(e); } } -} +} \ No newline at end of file diff --git a/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java b/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java index 04aebe6..83a8c6d 100644 --- a/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java @@ -1,6 +1,8 @@ package org.densy.scriptify.declaration; import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.declaration.util.TsImportUtil; import java.io.IOException; import java.nio.file.Files; @@ -20,15 +22,6 @@ public class ScriptTsProjectGenerator { */ public static final String TYPESCRIPT = "typescript"; - /** - * Script header comment. - */ - private static final String SCRIPT_HEADER_COMMENT = """ - /** - * Write your code below - */ - """; - /** * Type Script config with types declarations support. */ @@ -42,15 +35,17 @@ public class ScriptTsProjectGenerator { } """; + private final Script script; private final ScriptTsDeclarationGenerator generator; public ScriptTsProjectGenerator(Script script) { this.generator = new ScriptTsDeclarationGenerator(script); + this.script = script; } /** * Generates TS projects with tsconfig with declarations support. - *s + * * @param path Path of generated project */ public void generate(Path path) { @@ -59,18 +54,22 @@ public void generate(Path path) { /** * Generates TS projects with tsconfig with declarations support. - *s + * * @param path Path of generated project * @param language Scripting language (JavaScript and TypeScript supported) */ public void generate(Path path, String language) { Path src = path.resolve("src"); + Path types = path.resolve("types"); if (!path.toFile().exists()) { path.toFile().mkdirs(); if (!src.toFile().exists()) { src.toFile().mkdirs(); } + if (!types.toFile().exists()) { + types.toFile().mkdirs(); + } } String extension = switch (language.toLowerCase()) { @@ -81,11 +80,28 @@ public void generate(Path path, String language) { String declaration = generator.generate(); try { - Files.writeString(src.resolve("types.d.ts"), declaration); - Files.writeString(src.resolve("script." + extension), SCRIPT_HEADER_COMMENT); + Files.writeString(types.resolve("types.d.ts"), declaration); + Files.writeString(src.resolve("index." + extension), generateImports()); Files.writeString(path.resolve("tsconfig.json"), TYPE_SCRIPT_CONFIG); } catch (IOException e) { throw new RuntimeException(e); } } + + /** + * Generates imports template for script. + */ + private String generateImports() { + StringBuilder builder = new StringBuilder(); + + for (ScriptModule module : script.getModuleManager().getModules().values()) { + builder.append("import ") + .append(TsImportUtil.cleanupImportName(module.getName())) + .append(" from \"") + .append(module.getName()) + .append("\";\n"); + } + + return builder.toString(); + } } diff --git a/src/main/java/org/densy/scriptify/declaration/Test.java b/src/main/java/org/densy/scriptify/declaration/Test.java new file mode 100644 index 0000000..e5e2c2e --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/Test.java @@ -0,0 +1,49 @@ +package org.densy.scriptify.declaration; + +import org.densy.scriptify.api.script.module.ScriptInternalModule; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.common.script.module.StandardScriptModule; +import org.densy.scriptify.core.script.module.SimpleScriptInternalModule; +import org.densy.scriptify.js.graalvm.script.JsScript; + +import java.nio.file.Path; + +public class Test { + + public static void main(String[] args) { + JsScript script = new JsScript(); + script.getModuleManager().addModule(new StandardScriptModule()); + + + ScriptInternalModule in = new SimpleScriptInternalModule("internal"); + in.export(new ScriptValueExport("Value", Value.class)); + in.export(new ScriptValueExport("Type", Type.class)); + in.export(new ScriptValueExport("value", new Value("Test"))); + in.export(new ScriptValueExport("Inf", Inf.class)); + in.export(new ScriptValueExport("JsScript", JsScript.class)); + script.getModuleManager().addModule(in); + + ScriptTsProjectGenerator g = new ScriptTsProjectGenerator(script); + g.generate(Path.of("./proj")); + } + + public record Value(String val) { + + } + + public enum Type { + VALUE, + NUMBER, + STRING + } + + public interface Inf { + String DATA = "Data1231231"; + + String test(); + + default String doTest() { + return test(); + } + } +} diff --git a/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java b/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java new file mode 100644 index 0000000..68abafd --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java @@ -0,0 +1,30 @@ +package org.densy.scriptify.declaration.util; + +public final class TsImportUtil { + + public static String cleanupImportName(String name) { + if (name == null || name.isBlank()) { + return ""; + } + + String cleaned = name.replaceAll("^[^a-zA-Z0-9]+", "") + .replaceAll("[^a-zA-Z0-9]+", " "); + + StringBuilder result = new StringBuilder(); + String[] words = cleaned.split("\\s+"); + + for (int i = 0; i < words.length; i++) { + String word = words[i].toLowerCase(); + if (i == 0) { + result.append(word); + } else { + if (!word.isEmpty()) { + result.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)); + } + } + } + + return result.toString(); + } +} diff --git a/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java new file mode 100644 index 0000000..4c99f0f --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java @@ -0,0 +1,114 @@ +package org.densy.scriptify.declaration.writer; + +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +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.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionDefinitionExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.declaration.ScriptTsClassDeclarationGenerator; +import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ScriptTsExportDeclarationWriter { + + private final Script script; + private final ScriptTsClassDeclarationGenerator classGenerator; + + public ScriptTsExportDeclarationWriter(Script script, ScriptTsClassDeclarationGenerator classGenerator) { + this.script = script; + this.classGenerator = classGenerator; + } + + public void write( + StringBuilder builder, + ScriptExport export, + String indent, + boolean global, + Map, String> moduleClasses + ) { + if (export instanceof ScriptValueExport valueExport) { + writeValue(builder, valueExport, indent, global, moduleClasses); + } else if (export instanceof ScriptFunctionExport funcExport) { + ScriptFunctionDefinition definition = script.getFunctionManager() + .getFunctionDefinitionFactory() + .create(funcExport.getFunction()); + writeFunction(builder, definition, indent, global); + } else if (export instanceof ScriptFunctionDefinitionExport funcExport) { + writeFunction(builder, funcExport.getDefinition(), indent, global); + } else if (export instanceof ScriptConstantExport constExport) { + writeConstant(builder, constExport, indent, global); + } + } + + private void writeValue( + StringBuilder builder, + ScriptValueExport export, + String indent, + boolean global, + Map, String> moduleClasses + ) { + if (export.isClass()) { + Class clazz = (Class) export.getValue(); + String prefix = global ? "declare " : "export "; + builder.append(classGenerator.generate(clazz, indent, prefix)).append("\n"); + } else { + String prefix = global ? "declare const " : "export const "; + Class instanceClass = export.getValue().getClass(); + String tsType = moduleClasses.containsKey(instanceClass) + ? moduleClasses.get(instanceClass) + : JavaToTypeScriptConverter.convert(instanceClass); + builder.append(indent).append(prefix) + .append(export.getName()).append(": ").append(tsType).append(";\n"); + } + } + + private void writeFunction( + StringBuilder builder, + ScriptFunctionDefinition definition, + String indent, + boolean global + ) { + String prefix = global ? "declare function " : "export function "; + + for (var executor : definition.getExecutors()) { + builder.append(indent).append("/**\n") + .append(indent).append(" * ").append(definition.getFunction().getName()).append("\n"); + for (var arg : executor.getArguments()) { + builder.append(indent).append(" * @param ").append(arg.getName()) + .append(" ").append(arg.isRequired() ? "(required)" : "(optional)").append("\n"); + } + builder.append(indent).append(" */\n"); + + List params = new ArrayList<>(); + for (var arg : executor.getArguments()) { + params.add(arg.getName() + + (arg.isRequired() ? "" : "?") + + ": " + JavaToTypeScriptConverter.convert(arg.getType())); + } + + builder.append(indent).append(prefix) + .append(definition.getFunction().getName()).append("(") + .append(String.join(", ", params)).append("): ") + .append(JavaToTypeScriptConverter.convert(executor.getMethod().getReturnType())) + .append(";\n"); + } + } + + private void writeConstant( + StringBuilder builder, + ScriptConstantExport export, + String indent, + boolean global + ) { + String prefix = global ? "declare const " : "export const "; + Object value = export.getConstant().getValue(); + String tsType = JavaToTypeScriptConverter.convert(value != null ? value.getClass() : null); + builder.append(indent).append(prefix) + .append(export.getName()).append(": ").append(tsType).append(";\n"); + } +} \ No newline at end of file diff --git a/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java new file mode 100644 index 0000000..25e536c --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java @@ -0,0 +1,52 @@ +package org.densy.scriptify.declaration.writer; + +import org.densy.scriptify.api.script.module.ScriptInternalModule; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; + +import java.util.HashMap; +import java.util.Map; + +public class ScriptTsModuleDeclarationWriter { + + private final ScriptTsExportDeclarationWriter exportWriter; + + public ScriptTsModuleDeclarationWriter(ScriptTsExportDeclarationWriter exportWriter) { + this.exportWriter = exportWriter; + } + + public void writeGlobal(StringBuilder builder, ScriptInternalModule module) { + write(builder, module, "", true); + } + + public void writeNamed(StringBuilder builder, ScriptInternalModule module) { + builder.append("declare module \"").append(module.getName()).append("\" {\n"); + write(builder, module, " ", false); + builder.append("}\n\n"); + } + + private void write( + StringBuilder builder, + ScriptInternalModule module, + String indent, + boolean global + ) { + Map, String> moduleClasses = collectModuleClasses(module); + + for (ScriptExport export : module.getExports()) { + exportWriter.write(builder, export, indent, global, moduleClasses); + builder.append("\n"); + } + } + + private Map, String> collectModuleClasses(ScriptInternalModule module) { + Map, String> moduleClasses = new HashMap<>(); + for (ScriptExport export : module.getExports()) { + if (export instanceof ScriptValueExport valueExport && valueExport.isClass()) { + Class clazz = (Class) valueExport.getValue(); + moduleClasses.put(clazz, clazz.getSimpleName()); + } + } + return moduleClasses; + } +} \ No newline at end of file From 7b52d62242b36b3ec60bda701c44ac27f099138e Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sun, 26 Apr 2026 15:09:45 +0200 Subject: [PATCH 2/4] feat: option to generate declaration for all java classes + cleanup code --- .../DeclarationGeneratorOptions.java | 28 ++++ .../ScriptTsClassDeclarationGenerator.java | 32 +++- .../ScriptTsDeclarationGenerator.java | 78 +++++++++- .../declaration/ScriptTsProjectGenerator.java | 8 +- .../org/densy/scriptify/declaration/Test.java | 49 ------ .../util/ClassHierarchyCollector.java | 143 ++++++++++++++++++ .../declaration/util/TsImportUtil.java | 9 ++ .../ScriptTsExportDeclarationWriter.java | 24 ++- .../ScriptTsModuleDeclarationWriter.java | 2 + 9 files changed, 304 insertions(+), 69 deletions(-) create mode 100644 src/main/java/org/densy/scriptify/declaration/DeclarationGeneratorOptions.java delete mode 100644 src/main/java/org/densy/scriptify/declaration/Test.java create mode 100644 src/main/java/org/densy/scriptify/declaration/util/ClassHierarchyCollector.java diff --git a/src/main/java/org/densy/scriptify/declaration/DeclarationGeneratorOptions.java b/src/main/java/org/densy/scriptify/declaration/DeclarationGeneratorOptions.java new file mode 100644 index 0000000..1529c3e --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/DeclarationGeneratorOptions.java @@ -0,0 +1,28 @@ +package org.densy.scriptify.declaration; + +import lombok.Builder; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Set; + +/** + * Options for the declaration generator. + */ +@Builder +@Getter +public class DeclarationGeneratorOptions { + + @Builder.Default + private boolean generateClassHierarchy = false; + + @Builder.Default + private Set hierarchyExcludedPackages = new HashSet<>(Set.of( + "java.lang", + "java.io", + "java.nio" + )); + + @Builder.Default + private int hierarchyMaxDepth = 2; +} \ No newline at end of file diff --git a/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java b/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java index 99f95c3..3fb9d1f 100644 --- a/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java @@ -1,13 +1,31 @@ package org.densy.scriptify.declaration; +import lombok.Setter; import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter; import java.lang.reflect.*; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +@Setter public class ScriptTsClassDeclarationGenerator { + private Map, String> knownClasses = new HashMap<>(); + + private String resolveType(Type type) { + if (type instanceof Class clazz) { + if (knownClasses.containsKey(clazz)) return knownClasses.get(clazz); + } else if (type instanceof ParameterizedType pt) { + if (pt.getRawType() instanceof Class raw && knownClasses.containsKey(raw)) { + String rawName = knownClasses.get(raw); + String[] args = Arrays.stream(pt.getActualTypeArguments()) + .map(this::resolveType) + .toArray(String[]::new); + return rawName + "<" + String.join(", ", args) + ">"; + } + } + return JavaToTypeScriptConverter.convert(type); + } + public String generate(Class clazz, String indent, String prefix) { if (clazz.isEnum()) { return generateEnum(clazz, indent, prefix); @@ -37,7 +55,7 @@ private String generateClass(Class clazz, String indent, String prefix) { builder.append(indent).append(" "); if (Modifier.isFinal(field.getModifiers())) builder.append("readonly "); builder.append(field.getName()).append(": ") - .append(JavaToTypeScriptConverter.convert(field.getGenericType())) + .append(resolveType(field.getGenericType())) .append(";\n"); } @@ -50,7 +68,7 @@ private String generateClass(Class clazz, String indent, String prefix) { .append(method.getName()).append("(") .append(generateParams(method.getGenericParameterTypes(), method.getParameters())) .append("): ") - .append(JavaToTypeScriptConverter.convert(method.getGenericReturnType())) + .append(resolveType(method.getGenericReturnType())) .append(";\n"); } @@ -67,7 +85,7 @@ private String generateInterface(Class clazz, String indent, String prefix) { for (Field field : clazz.getFields()) { builder.append(indent).append(" readonly ") .append(field.getName()).append(": ") - .append(JavaToTypeScriptConverter.convert(field.getGenericType())) + .append(resolveType(field.getGenericType())) .append(";\n"); } @@ -79,7 +97,7 @@ private String generateInterface(Class clazz, String indent, String prefix) { .append(method.getName()).append("(") .append(generateParams(method.getGenericParameterTypes(), method.getParameters())) .append("): ") - .append(JavaToTypeScriptConverter.convert(method.getGenericReturnType())) + .append(resolveType(method.getGenericReturnType())) .append(";\n"); } @@ -109,7 +127,7 @@ private String generateParams(Type[] types, Parameter[] parameters) { String name = parameters[i].isNamePresent() ? parameters[i].getName() : "arg" + i; - params.add(name + ": " + JavaToTypeScriptConverter.convert(types[i])); + params.add(name + ": " + resolveType(types[i])); } return String.join(", ", params); } diff --git a/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java b/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java index d7d8120..f3735b0 100644 --- a/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java @@ -4,26 +4,38 @@ import org.densy.scriptify.api.script.module.ScriptInternalModule; 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.declaration.util.ClassHierarchyCollector; import org.densy.scriptify.declaration.writer.ScriptTsExportDeclarationWriter; import org.densy.scriptify.declaration.writer.ScriptTsModuleDeclarationWriter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; +import java.nio.file.*; +import java.util.*; public class ScriptTsDeclarationGenerator { private final Script script; + private final DeclarationGeneratorOptions options; private final ScriptTsModuleDeclarationWriter moduleWriter; + private final ScriptTsClassDeclarationGenerator classGenerator; + private final ClassHierarchyCollector hierarchyCollector; public ScriptTsDeclarationGenerator(Script script) { + this(script, DeclarationGeneratorOptions.builder().build()); + } + + public ScriptTsDeclarationGenerator(Script script, DeclarationGeneratorOptions options) { this.script = script; - ScriptTsClassDeclarationGenerator classGenerator = new ScriptTsClassDeclarationGenerator(); - ScriptTsExportDeclarationWriter exportWriter = new ScriptTsExportDeclarationWriter(script, classGenerator); + this.options = options; + this.classGenerator = new ScriptTsClassDeclarationGenerator(); + ScriptTsExportDeclarationWriter exportWriter = + new ScriptTsExportDeclarationWriter(script, classGenerator); this.moduleWriter = new ScriptTsModuleDeclarationWriter(exportWriter); - + this.hierarchyCollector = new ClassHierarchyCollector(options, script); } protected String getHeader() { @@ -41,10 +53,54 @@ public String generate() { StringBuilder builder = new StringBuilder(getHeader()); ScriptModuleManager moduleManager = script.getModuleManager(); + // collect all modules to process + List allModules = new ArrayList<>(); + allModules.add(moduleManager.getGlobalModule()); + for (ScriptModule module : moduleManager.getModules().values()) { + if (module instanceof ScriptInternalModule im) allModules.add(im); + } + + Map, String> knownClasses = new LinkedHashMap<>(); + + Set> registered = collectRegisteredClasses(allModules); + for (Class clazz : registered) { + knownClasses.put(clazz, clazz.getSimpleName()); + } + + if (options.isGenerateClassHierarchy()) { + Set> hierarchy = new LinkedHashSet<>(); + for (ScriptInternalModule module : allModules) { + hierarchy.addAll(hierarchyCollector.collect(module, registered)); + } + for (Class clazz : hierarchy) { + knownClasses.put(clazz, clazz.getSimpleName()); + } + + classGenerator.setKnownClasses(knownClasses); + moduleWriter.getExportWriter().setHierarchyClasses(knownClasses); + + if (!hierarchy.isEmpty()) { + builder.append("// --- Generated class hierarchy ---\n\n"); + for (Class clazz : hierarchy) { + builder.append(classGenerator.generate(clazz, "", "declare ")).append("\n\n"); + } + } + } else { + classGenerator.setKnownClasses(knownClasses); + moduleWriter.getExportWriter().setHierarchyClasses(knownClasses); + } + + // generate global and internal modules + if (!moduleManager.getGlobalModule().getExports().isEmpty()) { + builder.append("// --- Generated global internal module ---\n\n"); + } moduleWriter.writeGlobal(builder, moduleManager.getGlobalModule()); for (ScriptModule module : moduleManager.getModules().values()) { if (module instanceof ScriptInternalModule internalModule) { + builder.append("// --- Generated internal module \"") + .append(internalModule.getName()) + .append("\" ---\n\n"); moduleWriter.writeNamed(builder, internalModule); } } @@ -52,6 +108,18 @@ public String generate() { return builder.toString(); } + private Set> collectRegisteredClasses(List modules) { + Set> registered = new HashSet<>(); + for (ScriptInternalModule module : modules) { + for (ScriptExport export : module.getExports()) { + if (export instanceof ScriptValueExport ve && ve.isClass()) { + registered.add((Class) ve.getValue()); + } + } + } + return registered; + } + public void save(Path path) { try { Files.writeString(path, generate(), StandardCharsets.UTF_8); diff --git a/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java b/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java index 83a8c6d..af14f0b 100644 --- a/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsProjectGenerator.java @@ -39,7 +39,11 @@ public class ScriptTsProjectGenerator { private final ScriptTsDeclarationGenerator generator; public ScriptTsProjectGenerator(Script script) { - this.generator = new ScriptTsDeclarationGenerator(script); + this(script, DeclarationGeneratorOptions.builder().build()); + } + + public ScriptTsProjectGenerator(Script script, DeclarationGeneratorOptions options) { + this.generator = new ScriptTsDeclarationGenerator(script, options); this.script = script; } @@ -95,7 +99,7 @@ private String generateImports() { StringBuilder builder = new StringBuilder(); for (ScriptModule module : script.getModuleManager().getModules().values()) { - builder.append("import ") + builder.append("import * as ") .append(TsImportUtil.cleanupImportName(module.getName())) .append(" from \"") .append(module.getName()) diff --git a/src/main/java/org/densy/scriptify/declaration/Test.java b/src/main/java/org/densy/scriptify/declaration/Test.java deleted file mode 100644 index e5e2c2e..0000000 --- a/src/main/java/org/densy/scriptify/declaration/Test.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.densy.scriptify.declaration; - -import org.densy.scriptify.api.script.module.ScriptInternalModule; -import org.densy.scriptify.api.script.module.export.ScriptValueExport; -import org.densy.scriptify.common.script.module.StandardScriptModule; -import org.densy.scriptify.core.script.module.SimpleScriptInternalModule; -import org.densy.scriptify.js.graalvm.script.JsScript; - -import java.nio.file.Path; - -public class Test { - - public static void main(String[] args) { - JsScript script = new JsScript(); - script.getModuleManager().addModule(new StandardScriptModule()); - - - ScriptInternalModule in = new SimpleScriptInternalModule("internal"); - in.export(new ScriptValueExport("Value", Value.class)); - in.export(new ScriptValueExport("Type", Type.class)); - in.export(new ScriptValueExport("value", new Value("Test"))); - in.export(new ScriptValueExport("Inf", Inf.class)); - in.export(new ScriptValueExport("JsScript", JsScript.class)); - script.getModuleManager().addModule(in); - - ScriptTsProjectGenerator g = new ScriptTsProjectGenerator(script); - g.generate(Path.of("./proj")); - } - - public record Value(String val) { - - } - - public enum Type { - VALUE, - NUMBER, - STRING - } - - public interface Inf { - String DATA = "Data1231231"; - - String test(); - - default String doTest() { - return test(); - } - } -} diff --git a/src/main/java/org/densy/scriptify/declaration/util/ClassHierarchyCollector.java b/src/main/java/org/densy/scriptify/declaration/util/ClassHierarchyCollector.java new file mode 100644 index 0000000..b51099a --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/util/ClassHierarchyCollector.java @@ -0,0 +1,143 @@ +package org.densy.scriptify.declaration.util; + +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +import org.densy.scriptify.api.script.module.ScriptInternalModule; +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.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionDefinitionExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.declaration.DeclarationGeneratorOptions; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Class hierarchy collector for module declarations. + */ +public class ClassHierarchyCollector { + + private final DeclarationGeneratorOptions options; + private final Script script; + + public ClassHierarchyCollector(DeclarationGeneratorOptions options, Script script) { + this.options = options; + this.script = script; + } + + /** + * Collects the entire class hierarchy of the module. + * + * @param module internal script module + * @param alreadyRegistered already registered classes + * @return collected classes + */ + public Set> collect(ScriptInternalModule module, Set> alreadyRegistered) { + Set> collected = new LinkedHashSet<>(); + + for (ScriptExport export : module.getExports()) { + collectFromExport(export, collected, 0); + } + + collected.removeAll(alreadyRegistered); + return collected; + } + + private void collectFromExport(ScriptExport export, Set> collected, int depth) { + if (export instanceof ScriptValueExport valueExport) { + if (valueExport.isClass()) { + collectFromClass((Class) valueExport.getValue(), collected, depth); + } else { + collectClass(valueExport.getValue().getClass(), collected, depth); + } + } else if (export instanceof ScriptFunctionDefinitionExport funcExport) { + collectFromDefinition(funcExport.getDefinition(), collected, depth); + } else if (export instanceof ScriptFunctionExport funcExport) { + var definition = script.getFunctionManager() + .getFunctionDefinitionFactory() + .create(funcExport.getFunction()); + collectFromDefinition(definition, collected, depth); + } else if (export instanceof ScriptConstantExport constExport) { + Object value = constExport.getConstant().getValue(); + if (value != null) collectClass(value.getClass(), collected, depth); + } + } + + private void collectFromDefinition(ScriptFunctionDefinition definition, Set> collected, int depth) { + for (var executor : definition.getExecutors()) { + collectFromType(executor.getMethod().getGenericReturnType(), collected, depth); + for (var arg : executor.getArguments()) { + collectFromType(arg.getType(), collected, depth); + } + } + } + + private void collectFromClass(Class clazz, Set> collected, int depth) { + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == Object.class) continue; + collectFromType(method.getGenericReturnType(), collected, depth); + for (Type param : method.getGenericParameterTypes()) { + collectFromType(param, collected, depth); + } + } + for (Field field : clazz.getFields()) { + collectFromType(field.getGenericType(), collected, depth); + } + } + + private void collectClass(Class clazz, Set> collected, int depth) { + if (clazz == null + || clazz.isPrimitive() + || clazz == void.class + || clazz == Void.class + || clazz.isArray() + || collected.contains(clazz) + || isExcluded(clazz) + || isMappedToTsPrimitive(clazz)) { + return; + } + + if (options.getHierarchyMaxDepth() != -1 && depth >= options.getHierarchyMaxDepth()) { + return; + } + + collected.add(clazz); + + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == Object.class) continue; + collectFromType(method.getGenericReturnType(), collected, depth + 1); + for (Type param : method.getGenericParameterTypes()) { + collectFromType(param, collected, depth + 1); + } + } + for (Field field : clazz.getFields()) { + collectFromType(field.getGenericType(), collected, depth + 1); + } + } + + private void collectFromType(Type type, Set> collected, int depth) { + if (type instanceof Class clazz) { + collectClass(clazz, collected, depth); + } else if (type instanceof ParameterizedType pt) { + collectFromType(pt.getRawType(), collected, depth); + for (Type arg : pt.getActualTypeArguments()) { + collectFromType(arg, collected, depth); + } + } + } + + private boolean isExcluded(Class clazz) { + String packageName = clazz.getPackageName(); + return options.getHierarchyExcludedPackages().stream().anyMatch(packageName::startsWith); + } + + private boolean isMappedToTsPrimitive(Class clazz) { + String tsType = JavaToTypeScriptConverter.convert(clazz); + return !tsType.equals(clazz.getName()) && !tsType.equals(clazz.getSimpleName()); + } +} \ No newline at end of file diff --git a/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java b/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java index 68abafd..4d79cbf 100644 --- a/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java +++ b/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java @@ -1,7 +1,16 @@ package org.densy.scriptify.declaration.util; +/** + * TypeScript import utility. + */ public final class TsImportUtil { + /** + * Cleans up the import name so that it is suitable for a variable name. + * + * @param name raw import (module) name + * @return cleaned import name + */ public static String cleanupImportName(String name) { if (name == null || name.isBlank()) { return ""; diff --git a/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java index 4c99f0f..aa9654f 100644 --- a/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java +++ b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java @@ -1,5 +1,6 @@ package org.densy.scriptify.declaration.writer; +import lombok.Setter; import org.densy.scriptify.api.script.Script; import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; import org.densy.scriptify.api.script.module.export.ScriptExport; @@ -10,7 +11,9 @@ import org.densy.scriptify.declaration.ScriptTsClassDeclarationGenerator; import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter; +import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -18,12 +21,22 @@ public class ScriptTsExportDeclarationWriter { private final Script script; private final ScriptTsClassDeclarationGenerator classGenerator; + @Setter + private Map, String> hierarchyClasses = new HashMap<>(); public ScriptTsExportDeclarationWriter(Script script, ScriptTsClassDeclarationGenerator classGenerator) { this.script = script; this.classGenerator = classGenerator; } + private String resolveType(Type type, Map, String> moduleClasses) { + if (type instanceof Class clazz) { + if (moduleClasses.containsKey(clazz)) return moduleClasses.get(clazz); + if (hierarchyClasses.containsKey(clazz)) return hierarchyClasses.get(clazz); + } + return JavaToTypeScriptConverter.convert(type); + } + public void write( StringBuilder builder, ScriptExport export, @@ -58,10 +71,7 @@ private void writeValue( builder.append(classGenerator.generate(clazz, indent, prefix)).append("\n"); } else { String prefix = global ? "declare const " : "export const "; - Class instanceClass = export.getValue().getClass(); - String tsType = moduleClasses.containsKey(instanceClass) - ? moduleClasses.get(instanceClass) - : JavaToTypeScriptConverter.convert(instanceClass); + String tsType = resolveType(export.getValue().getClass(), moduleClasses); builder.append(indent).append(prefix) .append(export.getName()).append(": ").append(tsType).append(";\n"); } @@ -86,15 +96,17 @@ private void writeFunction( List params = new ArrayList<>(); for (var arg : executor.getArguments()) { + String tsType = resolveType(arg.getType(), Map.of()); params.add(arg.getName() + (arg.isRequired() ? "" : "?") - + ": " + JavaToTypeScriptConverter.convert(arg.getType())); + + ": " + tsType); } + String returnType = resolveType(executor.getMethod().getGenericReturnType(), Map.of()); builder.append(indent).append(prefix) .append(definition.getFunction().getName()).append("(") .append(String.join(", ", params)).append("): ") - .append(JavaToTypeScriptConverter.convert(executor.getMethod().getReturnType())) + .append(returnType) .append(";\n"); } } diff --git a/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java index 25e536c..f300a29 100644 --- a/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java +++ b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java @@ -1,5 +1,6 @@ package org.densy.scriptify.declaration.writer; +import lombok.Getter; import org.densy.scriptify.api.script.module.ScriptInternalModule; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.densy.scriptify.api.script.module.export.ScriptValueExport; @@ -7,6 +8,7 @@ import java.util.HashMap; import java.util.Map; +@Getter public class ScriptTsModuleDeclarationWriter { private final ScriptTsExportDeclarationWriter exportWriter; From d4c2474754c8dcb43f8ba799de74ada8bf24374c Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sun, 26 Apr 2026 15:11:00 +0200 Subject: [PATCH 3/4] chore: remove unnecessary imports --- build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 32877c5..908a2b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,8 +21,6 @@ repositories { dependencies { api("org.densy.scriptify:api:1.6.0-beta") api("org.densy.scriptify:core:1.6.0-beta") - api("org.densy.scriptify:script-js-graalvm:1.6.0-beta") - api("org.densy.scriptify:common:1.6.0-beta") compileOnlyApi("org.projectlombok:lombok:1.18.36") annotationProcessor("org.projectlombok:lombok:1.18.36") } From 85a8f759c16704135ec38120656be49e3e14647e Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sun, 26 Apr 2026 15:11:19 +0200 Subject: [PATCH 4/4] chore: bump version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 908a2b6..eb06be8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "org.densy.scriptify.declaration" -version = "1.0.1-SNAPSHOT" +version = "1.1.0-SNAPSHOT" java { toolchain {