From 8b601565c9e14aa673dca94bd79c84ad6f2f04f3 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sat, 19 Sep 2026 22:21:42 +0100 Subject: Add partial import support to typescript-types --- .../generators/typescript/TypescriptGenerator.java | 59 ++++++++++++++++++-- .../typescripttypes/TypescriptTypesGenerator.java | 65 ++++++++++++++++++---- 2 files changed, 109 insertions(+), 15 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescript/TypescriptGenerator.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescript/TypescriptGenerator.java index f19a641..25435c5 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescript/TypescriptGenerator.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescript/TypescriptGenerator.java @@ -3,12 +3,15 @@ package org.zwobble.hobgoblin.compiler.output.generators.typescript; import org.zwobble.hobgoblin.compiler.builtins.NativeTypes; import org.zwobble.hobgoblin.compiler.output.lang.typescript.TypescriptWriter; import org.zwobble.hobgoblin.compiler.output.lang.typescript.ast.*; +import org.zwobble.hobgoblin.compiler.typechecker.TypesInfo; import org.zwobble.hobgoblin.compiler.types.*; import org.zwobble.hobgoblin.compiler.util.Casing; import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.SequencedSet; import java.util.stream.Collectors; public class TypescriptGenerator { @@ -30,6 +33,35 @@ public class TypescriptGenerator { TypescriptWriter.writeTo(path, writer -> writer.writeModule(module)); } + public record RequiredType(SimpleType type, TypescriptIdentifier importName) {} + + public static class Context { + private final SequencedSet requiredTypes = new LinkedHashSet<>(); + private final NamespaceName namespaceName; + private final TypesInfo typesInfo; + + public Context(NamespaceName namespaceName, TypesInfo typesInfo) { + this.namespaceName = namespaceName; + this.typesInfo = typesInfo; + } + + public NamespaceName namespaceName() { + return this.namespaceName; + } + + public void requireType(SimpleType type, TypescriptIdentifier importName) { + this.requiredTypes.add(new RequiredType(type, importName)); + } + + public SequencedSet requiredTypes() { + return requiredTypes; + } + + public TypesInfo typesInfo() { + return typesInfo; + } + } + public TypescriptIdentifier generateModuleName(String namespaceNamePart) { return new TypescriptIdentifier(namespaceNamePart); } @@ -46,13 +78,13 @@ public class TypescriptGenerator { return new TypescriptIdentifier(Casing.lowerCamelCaseToUpperCamelCase(name)); } - public TypescriptExpression generateTypeExpression(Type value) { + public TypescriptExpression generateTypeExpression(Type value, Context context) { return switch (collapseType(value)) { case ConstructedType constructedType -> { yield new TypescriptConstructedType( - generateTypeExpression(constructedType.constructor().genericType()), + generateTypeExpression(constructedType.constructor().genericType(), context), constructedType.args().stream() - .map(this::generateTypeExpression) + .map(argType -> generateTypeExpression(argType, context)) .toList() ); } @@ -65,7 +97,26 @@ public class TypescriptGenerator { } } - yield new TypescriptRef(this.generateTypeName(simpleType.name())); + var typescriptName = this.generateTypeName(simpleType.name()); + + if ( + !simpleType.namespaceName().isRoot() && + !simpleType.namespaceName().equals(context.namespaceName()) + ) { + var importName = new StringBuilder(); + importName.append("$ns$"); + for (var namespaceNamePart : simpleType.namespaceName().parts()) { + importName.append(this.generateModuleName(namespaceNamePart).value()); + importName.append("$"); + } + importName.append(this.generateTypeName(simpleType.name()).value()); + + typescriptName = TypescriptIdentifier.of(importName.toString()); + + context.requireType(simpleType, typescriptName); + } + + yield new TypescriptRef(typescriptName); } case TypeLevelValueType _ -> { diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescripttypes/TypescriptTypesGenerator.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescripttypes/TypescriptTypesGenerator.java index ac8e93a..dafb0d2 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescripttypes/TypescriptTypesGenerator.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/typescripttypes/TypescriptTypesGenerator.java @@ -19,6 +19,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; public class TypescriptTypesGenerator implements Generator { private static final String NAME = "typescript-types"; @@ -85,16 +86,49 @@ public class TypescriptTypesGenerator implements Generator { .map(part -> this.typescriptGenerator.generateModuleName(part)) .toList(); + // We could translate hobgoblin imports into TypeScript imports + // directly: for instance, `import {DocComment} from ast;` could be + // translated into `import {DocComment} from "./ast"`. However, given + // there may be custom code, we want to avoid conflicting with that + // custom code, or having the custom code depending on the generated + // imports. Therefore, track the uses of imported names and rename them + // to be fully qualified, for instance: + // `import {DocComment as $ns$ast$DocComment} from "./ast"` + + var context = new TypescriptGenerator.Context(namespace.namespaceName(), typesInfo); + var statements = namespace.body().stream() - .flatMap(statement -> generateNamespaceStatement(statement, typesInfo).stream()) + .flatMap(statement -> generateNamespaceStatement(statement, context).stream()) .toList(); - return new TypescriptModule(typescriptModuleName, List.of(), statements); + // TODO: combine imports from same module + + var imports = context.requiredTypes().stream() + .map(requiredType -> { + var exportName = this.typescriptGenerator.generateTypeName(requiredType.type().name()); + + return new TypescriptImportDeclaration( + List.of( + new TypescriptNamedImport( + exportName, + requiredType.importName() + ) + ), + // TODO: handle imports from parent + "./" + requiredType.type().namespaceName().parts().stream() + .map(part -> this.typescriptGenerator.generateModuleName(part).value()) + // TODO: handle extensions + .collect(Collectors.joining("/")) + ".ts" + ); + }) + .toList(); + + return new TypescriptModule(typescriptModuleName, imports, statements); } private List generateNamespaceStatement( TypedNamespaceStatementNode statement, - TypesInfo typesInfo + TypescriptGenerator.Context context ) { switch (statement) { case TypedEnumDefinitionNode enumDefinition -> { @@ -106,11 +140,11 @@ public class TypescriptTypesGenerator implements Generator { } case TypedStructDefinitionNode structDefinition -> { - return generateStructDefinition(structDefinition, typesInfo); + return generateStructDefinition(structDefinition, context); } case TypedSumDefinitionNode sumDefinition -> { - return generateSumDefinition(sumDefinition, typesInfo); + return generateSumDefinition(sumDefinition, context); } } } @@ -188,7 +222,7 @@ public class TypescriptTypesGenerator implements Generator { private List generateStructDefinition( TypedStructDefinitionNode structDefinition, - TypesInfo typesInfo + TypescriptGenerator.Context context ) { var typescriptTypeName = this.typescriptGenerator.generateTypeName(structDefinition.name()); @@ -206,7 +240,7 @@ public class TypescriptTypesGenerator implements Generator { var body = new ArrayList(); - if (!typesInfo.variantOf(structDefinition.typeOrConstructor().unboundType()).isEmpty()) { + if (!context.typesInfo().variantOf(structDefinition.typeOrConstructor().unboundType()).isEmpty()) { body.add(new TypescriptClassFieldDeclaration( TAG_PROPERTY_NAME, TypescriptVisibility.PUBLIC, @@ -238,7 +272,10 @@ public class TypescriptTypesGenerator implements Generator { TypescriptVisibility.PUBLIC, false, true, - Optional.of(this.typescriptGenerator.generateTypeExpression(field.type().value())), + Optional.of(this.typescriptGenerator.generateTypeExpression( + field.type().value(), + context + )), Optional.empty() )); } @@ -266,7 +303,10 @@ public class TypescriptTypesGenerator implements Generator { structDefinition.fields().orElse(List.of()).stream() .map(field -> new TypescriptParam( this.typescriptGenerator.generateFieldName(field.name()), - Optional.of(this.typescriptGenerator.generateTypeExpression(field.type().value())) + Optional.of(this.typescriptGenerator.generateTypeExpression( + field.type().value(), + context + )) )) .toList(), structDefinition.fields().orElse(List.of()).stream() @@ -294,7 +334,7 @@ public class TypescriptTypesGenerator implements Generator { private List generateSumDefinition( TypedSumDefinitionNode sumDefinition, - TypesInfo typesInfo + TypescriptGenerator.Context context ) { var typescriptTypeName = this.typescriptGenerator.generateTypeName(sumDefinition.name()); @@ -303,7 +343,10 @@ public class TypescriptTypesGenerator implements Generator { typescriptTypeName, new TypescriptTypeUnion( sumDefinition.variants().stream() - .map(variant -> this.typescriptGenerator.generateTypeExpression(variant.type().value())) + .map(variant -> this.typescriptGenerator.generateTypeExpression( + variant.type().value(), + context + )) .toList() ) ), -- cgit v1.2.3