diff --git a/build.gradle.kts b/build.gradle.kts index cfb47f2..eb06be8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,11 +5,11 @@ plugins { } group = "org.densy.scriptify.declaration" -version = "1.0.1-SNAPSHOT" +version = "1.1.0-SNAPSHOT" java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } @@ -19,7 +19,8 @@ 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") compileOnlyApi("org.projectlombok:lombok:1.18.36") annotationProcessor("org.projectlombok:lombok:1.18.36") } 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 new file mode 100644 index 0000000..3fb9d1f --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsClassDeclarationGenerator.java @@ -0,0 +1,134 @@ +package org.densy.scriptify.declaration; + +import lombok.Setter; +import org.densy.scriptify.declaration.util.JavaToTypeScriptConverter; + +import java.lang.reflect.*; +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); + } + 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(resolveType(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(resolveType(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(resolveType(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(resolveType(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 + ": " + resolveType(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..f3735b0 100644 --- a/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java +++ b/src/main/java/org/densy/scriptify/declaration/ScriptTsDeclarationGenerator.java @@ -1,37 +1,43 @@ 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.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.util.ArrayList; -import java.util.List; - -/** - * Generator for Scriptify functions declarations. - */ +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; + this.options = options; + this.classGenerator = new ScriptTsClassDeclarationGenerator(); + ScriptTsExportDeclarationWriter exportWriter = + new ScriptTsExportDeclarationWriter(script, classGenerator); + this.moduleWriter = new ScriptTsModuleDeclarationWriter(exportWriter); + this.hierarchyCollector = new ClassHierarchyCollector(options, script); } - /** - * 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 +49,82 @@ 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"); - } + 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); } - // 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"); - - sb.append("declare function ") - .append(def.getFunction().getName()) - .append("("); - - 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"); + 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); + } } - return sb.toString(); + 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; } - /** - * 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..af14f0b 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,21 @@ public class ScriptTsProjectGenerator { } """; + private final Script script; 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; } /** * Generates TS projects with tsconfig with declarations support. - *s + * * @param path Path of generated project */ public void generate(Path path) { @@ -59,18 +58,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 +84,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 * as ") + .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/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 new file mode 100644 index 0000000..4d79cbf --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/util/TsImportUtil.java @@ -0,0 +1,39 @@ +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 ""; + } + + 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..aa9654f --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsExportDeclarationWriter.java @@ -0,0 +1,126 @@ +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; +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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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, + 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 "; + String tsType = resolveType(export.getValue().getClass(), moduleClasses); + 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()) { + String tsType = resolveType(arg.getType(), Map.of()); + params.add(arg.getName() + + (arg.isRequired() ? "" : "?") + + ": " + 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(returnType) + .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..f300a29 --- /dev/null +++ b/src/main/java/org/densy/scriptify/declaration/writer/ScriptTsModuleDeclarationWriter.java @@ -0,0 +1,54 @@ +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; + +import java.util.HashMap; +import java.util.Map; + +@Getter +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