frhagn / Typewriter

Automatic TypeScript template generation from C# source files
http://frhagn.github.io/Typewriter
Apache License 2.0
536 stars 132 forks source link

Namespace import issue angular #275

Open drony opened 6 years ago

drony commented 6 years ago

//school.ts module ServiceModule { export class School { public name: string; } }

// student.ts module ServiceModule { export class Student{ public name: string; public school: School; // <--- Reference to school.ts } }

OPTION 1: //app.component.ts import {Student} from '../app/interfaces/Student' // ERROR IN THIS LINE => '/src/app/interfaces/Student.ts' is not a module.

export class AppComponent implements OnInit, OnDestroy {

private st: Student; }

how to handle imports with typewriter.

OPTION 2: export namespace A { } export namespace A { } import * as path from '../app/interfaces/Student.cs'; // ERROR IN THIS LINE => '/src/app/interfaces/Student.ts' is not a module.

OPTION 3: module, namespace, export module, export namespace combinations also doesn't work

mcaden commented 6 years ago

I'm having the same problem. I think the real issue is one of learning curve because there are a lot of things you simply straight up have to write yourself.

I'm writing an Angular 6 app and I've got imports working. I'm using a custom attribute to mark the model classes I want this to operate on and using a custom function to formulate imports.

The function I defined FormulateImports gets called with some TypeWriter magic inside the $Classes template with the Class passed as a parameter automagically. Armed with that, I was able to use the Type information to figure out what needs to be imported. It makes a lot of assumptions but so far it's working for me. I do some checks to make sure the same type doesn't get imported twice but I can already think of use-cases I missed like a self-referencing class but it should be simple to take care of.

Hope it helps!

classes:

${
    using System.Text;

    void CreateImport(HashSet<string> importedTypes, StringBuilder imports, Type type)
    {
        if(importedTypes.Contains(type.Name) || type.Namespace.StartsWith("System"))
        {
            return;
        }

        importedTypes.Add(type.Name);
        imports.AppendLine($"import {{ {type.Name} }} from './{type.Name}';"); 
    }

    string FormulateImports(Class currentClass)
    {
        HashSet<string> importedTypes = new HashSet<string>();
        var properties = currentClass.Properties;
        StringBuilder imports = new StringBuilder();
        foreach(var prop in currentClass.Properties)
        {
            CreateImport(importedTypes, imports, prop.Type);
        }

        return imports.ToString();
    }
}$Classes([TypeScriptModel])[// AutoGenerated File
$FormulateImports

export class $Name {$Properties[public $name: $Type;
]
}]

And I've got interfaces too which means I need to handle not just properties, but also methods. Here's what I've got there:

${
    using System.Text;

    void CreateImport(HashSet<string> importedTypes, StringBuilder imports, Type type)
    {
        if(importedTypes.Contains(type.Name) || type.Namespace.StartsWith("System"))
        {
            return;
        }

        importedTypes.Add(type.Name);
        imports.AppendLine($"import {{ {type.Name} }} from './{type.Name}';"); 
    }

    string FormulateImports(Interface current)
    {
        HashSet<string> importedTypes = new HashSet<string>();
        var properties = current.Properties;
        StringBuilder imports = new StringBuilder();
        foreach(var prop in current.Properties)
        {
            CreateImport(importedTypes, imports, prop.Type);
        }

        foreach(var method in current.Methods)
        {
            CreateImport(importedTypes, imports, method.Type);
            foreach(var p in method.Parameters)
            {
                CreateImport(importedTypes, imports, p.Type);
            }
        }

        return imports.ToString();
    }
}$Interfaces([SidebarTypeScriptModel])[// AutoGenerated File
$FormulateImports

export interface $Name {$Properties[
    public $name: $Type;
]$Methods[
    $name($Parameters[$name: $Type][, ]): $Type;]
}]
mcaden commented 5 years ago

Just following up on this with a much-improved version. This handles enums, interfaces, and classes along with proper import statements. This includes generic collections so it should handle arrays/lists/enumerables of a custom type.

The only thing I wish I could do is break it out to separate files without duplicating the import logic. To figure out what it needs to "import" it's using type.isDefined which checks to see if it's defined in the solution (according to the documentation at least). This works great for me as my models are in my solution but if you're wanting to generate against types in a library you may have to modify this.

${
    using System.Text;

    Template(Settings settings)
    {
        settings
            .IncludeCurrentProject()
            .IncludeProject("MyProject.Data"));
    }

    void GenerateImportsForType(Typewriter.CodeModel.Type type, StringBuilder imports, HashSet<string> importMap)
    {
        if(type.TypeArguments.Any())
        {
            foreach(var subType in type.TypeArguments)
            {
                GenerateImportsForType(subType, imports, importMap);
            }
        }
        else if(type.IsDefined && !importMap.Contains(type.Name))
        {
            imports.AppendLine($"import {{ {type.Name} }} from './{type.Name}';");
            importMap.Add(type.Name);
        }
    }

    string FormulateClassImports(Class currentClass)
    {
        HashSet<string> importMap = new HashSet<string> { currentClass.Name };
        StringBuilder imports = new StringBuilder();
        foreach(var prop in currentClass.Properties)
        {
            GenerateImportsForType(prop.Type, imports, importMap);
        }

        return imports.ToString();
    }

    string FormulateInterfaceImports(Interface currentInterface)
    {
        HashSet<string> importMap = new HashSet<string> { currentInterface.Name };
        StringBuilder imports = new StringBuilder();
        foreach(var prop in currentInterface.Properties)
        {
            GenerateImportsForType(prop.Type, imports, importMap);
        }

        foreach(var method in currentInterface.Methods)
        {
            GenerateImportsForType(method.Type, imports, importMap);
            foreach(var p in method.Parameters)
            {
                GenerateImportsForType(p.Type, imports, importMap);
            }
        }

        return imports.ToString();
    }
}$Classes([TypeScriptModel])[// AutoGenerated File
$FormulateClassImports
export class $Name {$Properties[
    $name: $Type;]
}]$Enums([TypeScriptModel])[// AutoGenerated File

export enum $Name {$Values[
    $Name = $Value][,]
}]$Interfaces([TypeScriptModel])[// AutoGenerated File
$FormulateInterfaceImports
export interface $Name {$Properties[
    public $name: $Type;
]$Methods[
    $name($Parameters[$name: $Type][, ]): $Type;]
}]
b2k commented 5 years ago

I updated the example from @mcaden to follow angular naming conventions.

var camelReg = new Regex("[A-Z]");
var tsReg = new Regex(".\\w+$");

settings.OutputFilenameFactory = file => tsReg.Replace(camelReg.Replace(file.Name, match => "-" + match.ToString().ToLower()).TrimStart('-'),".ts");

void GenerateImportsForType(Typewriter.CodeModel.Type type, StringBuilder imports, HashSet<string> importMap)
{
    var camelReg = new Regex("[A-Z]");

    if(type.TypeArguments.Any())
    {
        foreach(var subType in type.TypeArguments)
        {
            GenerateImportsForType(subType, imports, importMap);
        }
    }
    else if(type.IsDefined && !importMap.Contains(type.Name))
    {
        var fileName = camelReg.Replace(type.Name, match => "-" + match.ToString().ToLower()).TrimStart('-');
        imports.AppendLine($"import {{ {type.Name} }} from './{fileName}';");
        importMap.Add(type.Name);
    }
}