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

Output multiple types to single TS file? #75

Open jonstelly opened 8 years ago

jonstelly commented 8 years ago

I'd like to have one .ts file generated per .tst file. So if I had a single .tst template for all classes in NamespaceXYZ, instead of one .ts file for each class in that namespace, I'd like a single .ts file that contained all of the classes.

I don't think that's currently possible, but maybe I'm just missing it?

It seems like I could put all of my C# classes inside a single source file and that does what I'm looking for, but I don't like that for code organization reasons.

frhagn commented 8 years ago

This is not possible at the moment (other than putting all C# classes in a single file). It would be a nice feature, but it's a hard one to implement, so maybe in a future version...

kamranayub commented 8 years ago

I was just thinking about opening an issue about this--I agree it definitely creates a ton of files if you have a lot of models and it'd be nice to just concat the output into a single file :+1: Couldn't there be a post-build step in the template pipeline that just concats all the output?

johannordin commented 8 years ago

While I agree it might be a nice feature, but what happens when a class is changed? Either TypeWriter must find the output for that one class and change it (seems hard and troublesome), or re-render all the classes again (I imagine this is a costly action, depending on your tst-filter). @frhagn what is the cost of writing to file vs generating file content?

frhagn commented 8 years ago

There are two main issues to get this working. As @johannordin points out, when a file changes it needs to update a part of the output file or re-render all files. The other issue is tracking the path of the source files which at the moment only works for mapping a single output file to a single source file.

I'm working on support for this, but it's a major change so it'll be a while..

michaelaird commented 8 years ago

I would favour this over automatically re-rendering files.

ie. take the T4 Template approach and only re-render the output when the user right-clicks and chooses "run custom tool". TypeWriter could then find all the dependencies and re-render.

kamranayub commented 8 years ago

I just used typewriter in a demo and using tsconfig with compile on save already doesn't trigger compilation when saving a tst file, so I think not automatically rendering would be even more confusing. It took me a minute to realize I had to open the generated TS files and hit save to get VS to compile. So I would vote to at least keep the option to autogenerate.

On Sat, Mar 5, 2016, 06:00 Michael Aird notifications@github.com wrote:

I would favour this over automatically re-rendering files.

ie. take the T4 Template approach and only re-render the output when the user right-clicks and chooses "run custom tool". TypeWriter could then find all the dependencies and re-render.

— Reply to this email directly or view it on GitHub https://github.com/frhagn/Typewriter/issues/75#issuecomment-192626835.

michaelaird commented 8 years ago

Maybe an option in properties to say if this tst should autogenerate or manual. And multi-to-one is only supported by manual generation?

On Saturday, 5 March 2016, Kamran Ayub notifications@github.com wrote:

I just used typewriter in a demo and using tsconfig with compile on save already doesn't trigger compilation when saving a tst file, so I think not automatically rendering would be even more confusing. It took me a minute to realize I had to open the generated TS files and hit save to get VS to compile. So I would vote to at least keep the option to autogenerate.

On Sat, Mar 5, 2016, 06:00 Michael Aird <notifications@github.com javascript:_e(%7B%7D,'cvml','notifications@github.com');> wrote:

I would favour this over automatically re-rendering files.

ie. take the T4 Template approach and only re-render the output when the user right-clicks and chooses "run custom tool". TypeWriter could then find all the dependencies and re-render.

— Reply to this email directly or view it on GitHub https://github.com/frhagn/Typewriter/issues/75#issuecomment-192626835.

— Reply to this email directly or view it on GitHub https://github.com/frhagn/Typewriter/issues/75#issuecomment-192670229.

aluanhaddad commented 8 years ago

My personal reason for wanting this is to reduce the number of files that need to be imported/referenced from external TypeScript projects (I am generating .d.ts files containing interfaces that describe JSON endpoints). One solution might be to continue to generate output as is, but to additionally generate and maintain a single file, say models.d.ts, that imports or references each file created by Typewriter. This file would only need to be updated when types are added, removed, or moved, so it would not pose the same difficulty.

Anyway, I love this project. Keep up the awesome work!

kb3eua commented 8 years ago

I'm currently using TypeWriter to generate all of my Angular2 Services. But it'd be nice if I could also generate a single file that imports all of my dynamically generated services so that I can put them into one single PROVIDERS array when the application is bootstrapped instead of having to manually add them.

RolfVeinoeSorensen commented 8 years ago

kb3eua could I pursuade you to share the template you have created for Angular 2 services? I wish to create a template for asp.net 5 api controllers to autogen service ts files for Angular 2 services.

kb3eua commented 8 years ago

Rolf, here is my template for services. It will only pull in controllers from certain namespaces and that have an attribute named "TypeScriptGenerate"... that way I have control over which controllers I create services for and which ones I don't.

All of my models are in a "models" sibling folder, so I scan all of the return types and input parameters of my controller actions and import models as needed.

I also have slightly different method signatures depending on whether the method:

For non-primitive models that are returned, I have a "ModelHelper" utility that I use to convert the JSON to a strongly typed instance of that class, which also handles converting ISO date strings from the JSON to actual JavaScript Date objects using moment.js.

Services.txt

RolfVeinoeSorensen commented 8 years ago

Very nice template. Thank you for sharing!

jods4 commented 8 years ago

Was going to open an issue for merging several .cs files into a single .ts/.js output.

One motivation that was not mentionned in this thread is that if you emit ES6 w/ modules, then your file name defines your module name. So unlike C#, file structure in ES6 modules matters a lot.

For example, in C# I write lots of entities in a Model folder, one file per entity. In TS I would like to have a single generated model module from which I can do:

import { Order, Product, Customer } from 'model';

Rather than having a file for each one:

import { Order } from 'model/Order';
import { Product } from 'model/Product';
import { Customer } from 'model/Customer';
Oblomoff commented 8 years ago

+1 Well done, but make some hack, please. I cannot generate barrels like this. :(

export * from './entities'
export * from './application-bus'
export * from './message.service'
export * from './property.service'
export * from './elapsed.pipe'
rvalimaki commented 8 years ago

Hello,

I did customize the file name factory so that it exports files with "___.ts". Then I did create a Node.js script to concatenate all the files with same namespace.

Actually I did already shorten and combine namespaces so that we only had some 40 files for about 1000+ exported DataContract classes and enumerations instead of some 100+. When we had over 3 parts in the namespace "foo.bar.baz.x.y", I just shortened them to "foo.bar.baz". It would have been much easier to combine everything to one mega sized file, ecause then you would not need about the imports, but that would have been way too messy in our case due to sheer size of that file, and possible name clashes as well.

Additionally I had to generate imports for these files in Typewriter template, when our types did extend or use types from an another namespaces. And I did generate constructors, and that was a bit complicated since you have to traverse base classes recursively to get all the derived properties of baseclass and of baseclass of the baseclass and of baseclass of the baseclass of the baseclass... you got it ;-)

Ultimately I had to solve the problem with ordering of imports and class definition in combined files, since you cannot have duplicate imports and you have to define "class A" before "class B extends A" and that class B before "class C extends B", because of the limitation in either Typescript compiler or in Webpack (or similar), I'm not quite sure about the reason. Compiler doesn't give a warning, but it simply won't work.

For reason unknown, I had to add some missing imports "manually". Most of these were imports from a project to another, so maybe that's expected, but some where inside the very same project. Out of ~200+ imports to generate, only a handful had to be added hard coded to the file combining & type import, so not a big issue.

To summarize, I needed to alter the Typewriter template/settings to: -change the output file names prefixed with namespaces -include imports from other namespaces (-add derived properties to constructors)

And then I had to do an external script to: -combine these namespace-prefixed files into .ts files simply to ".ts" -remove duplicate imports from these files -reorder classes so that derived classes are defined after their base classes

If you alternatively combine everything into a single file, you don't need to worry about imports, but the class defining is still important, unless you don't have any derived classes.

rvalimaki commented 8 years ago

Sorry, file names in above posts where including special characters, so they got stripped of. So I did change Typewriter output file names to "(namespace)_(originalfilename).ts". And ultimately combined them to "(namespace).ts".

rickbatka commented 8 years ago

Back to the topic at hand, I'd like to add a bump for the idea of compiling to a single .ts file, but for a different reason than what's been stated so far.

We publish our web projects directly from Visual Studio, and in the interest of keeping generated files from cluttering the project files, we have typescript compile to one single .JS file and we don't check that file into version control. Same with our LESS files - we're set up to compile to a single CSS file, but we don't check in the outputted CSS file. This way, our version control only has the true "source" files, and we don't have problems with version control conflicts in generated files.

Typewriter doesn't work with this approach because when we update from VCS and get additional entries in the project file, but not the actual files, Typewriter is not clever enough to notice the missing files and regenerate them. Instead, I have to right-click the template file and click "Render Template". This is a really only a minor annoyance, but it's inconsistent with the rest of the Visual Studio / Web Compiler tools, which will automatically generate the missing JS / CSS files the first time I attempt to build the project after a fresh checkout. As it stands, I update from VCS, build, and get build errors for the missing generated TS files. Then I manually re-render the typewriter files, build again, and it works.

Knowing that our workflow isn't the most common, I tried a workaround: I edited our project file to include everything in the typewriter directory via glob pattern, like this: <TypeScriptCompile Include="app\dto\gen\*.ts" />, and I simultaneously added all those generated ts files to our version control's "ignore" list. That appeared to work. Now we could add new files, see the .TS files get automatically generated and see them included in visual studio. My thinking was that, instead of having .proj file entries for files that didn't exist, the files would simply be missing from the project altogether and typewriter would notice and regenerate them right off the bat. The problem with this approach was, once typewriter had noticed the change and rendered the new files, it had gone into the .proj file and obliterated our glob pattern Include and re-entered individual entries for all the generated files.

Either compiling to one single .TS file or improving Typewriter's handling of broken references / missing files in the .proj file would fix our issue. As it stands, our workaround is to check in the generated files. If that's how it has to stay, no big deal, this project is absolutely awesome and it's not that painful of a workaround. Just thought I'd add some more justification for the single file idea. If we're forced to check in generated code, I'd rather have it contained in one file then deal with an entire directory of generated files.

michaelaird commented 8 years ago

I've been looking at NSwag lately (https://github.com/NSwag/NSwag). It looks very promising for generating a full TypeScript client for webapi controllers.

aluanhaddad commented 8 years ago

While I initially chimed in asking for this feature, I don't think it is particularly necessary. The separate files actually make things quite clear and I just generate a file which re-exports all of them on build so they can be conveniently referenced. It turns out that, contrary to what I said, it wasn't the number of files that was the issue, it was simply a need for better organization. One I stopped putting my generated TypeScript files into a globally visible namespaces, I stopped wanting this feature.

Here is my somewhat convoluted post-build script (powershell is not my strong point)

# clean re-exports
Remove-Item -Path .\..\..\models.ts -Force -ErrorAction Ignore

# clean target dir
ls .\..\..\..\..\web\src\models\*.ts | Remove-Item -Force -ErrorAction Ignore

# collect individual files
$dts = ls .\..\..\TypeScriptModels\*.ts -Exclude *.d.ts
# generate re-exports
[IO.File]::WriteAllText(
    '.\..\..\models.ts',
    "import { Moment } from 'moment';`n"  + "export * from './models/" +
            [string]::Join("export * from './models/",
    ($dts| % {$_.name.substring(0, $_.name.length - 3) + "';`n"})),
    [System.Text.Encoding]::UTF8
)
#copy files
$dts | Copy-Item -Force -Destination .\..\..\..\..\web\src\models
ls ..\..\models.ts | Copy-Item -Force -Destination .\..\..\..\..\web\src\models.ts

# clean re-exports
Remove-Item -Path .\..\..\models.ts -Force -ErrorAction Ignore
sonuautade commented 7 years ago

@frhagn any updates on this issue?

patrykbuzowicz commented 7 years ago

@frhagn I'm loving this piece of code. Therefore bumping this thread - we have a web project that aggregates models and we share the typings with linking the generated .d.ts from that aggregating project. It would be really great to be able to link a single file with all these models :)

aluanhaddad commented 7 years ago

@patrykbuzowicz it's easy to generate such a file by following the re-export pattern. It would be great if Typewriter did this automatically, It's very easy to do manually, provided you organize things in an amenable fashion.

patrykbuzowicz commented 7 years ago

I really liked the idea of glob include in csproj - I think it should suit the needs that we have. I wanted to bump the thread to see if there's anything planned in that area. Anyway, what we found more interesting is what the other issue is about: https://github.com/frhagn/Typewriter/issues/171

FlaynGordt commented 6 years ago

I am simply doing a copy /b *.tsx generated.ts after generating all the files.

simeyla commented 5 years ago

Real shame this isn't possible :-(

I'm already using NSwag which is amazing, but I wanted something simple to just create SignalR interface files but only to one file.

niikoo commented 5 years ago

+1

jwisener commented 4 years ago

+1

dreeco commented 4 years ago

+1

dreeco commented 4 years ago

I have found a potentially very bad solution, but it can save some headaches for small projects...

I needed to generate my SignalR hub definition and its parameters models definitions

Iterating through model classes, I build up a static string containing all my imports and generate typescript interfaces.

Then I import all my models (contained in different files) at once by using the static string.

It goes like

${
    using Typewriter.Extensions.Types;

    Template(Settings settings)
    {
        settings.IncludeCurrentProject();
    }

    static String allImports = "";
    string Imports(Class c)
    {
        allImports += $"import {{ {c.Name} }} from './{c.Name}'" + Environment.NewLine;
        return null;
    }

    string AllImports(Class c) 
    {
        return allImports;
    }
}$Classes(MyHub.*Model)[$Properties[$Type[$IsPrimitive[][import {$Name} from './$Name';
]]]
$Imports[]export interface $Name {$Properties[
    $name: $Type;]
}]$Classes(MyHub.Hub)[$AllImports
export class Hub implements $Interfaces(i=>i.Methods.Count>0)[$Name][,] {
    constructor(private connection: any){}$Interfaces(i=> i.Methods.Count>0)[
    $Methods[
  public $name($Parameters[$name: $Type][, ]) {
    return this.connection.invoke('$name', model);
  }
]]}
    $Interfaces(i=> i.Methods.Count>0)[
export interface $Name {$Methods[
    $name($Parameters[$name: $Type][, ]): void;]
}]$BaseClass[$TypeArguments[

export interface $Name{$Methods[
    $name($Parameters[$name: $Type][, ]): void;]
}]]]
Cogax commented 3 years ago

I use the following in my .csproj which solves me that problem when running a build:

  <Target Name="CopyGeneratedFiles" AfterTargets="BeforeBuild">
    <ItemGroup>
      <TypeScriptFiles Include="**\*.ts" />
    </ItemGroup>

    <ReadLinesFromFile File="%(TypeScriptFiles.Identity)">
      <Output TaskParameter="Lines" ItemName="FileLines" />
    </ReadLinesFromFile>

    <WriteLinesToFile File="generated.ts" Lines="@(FileLines)" Overwrite="true" />
  </Target>