Skip to content

Commit 2a30025

Browse files
author
Denis Peshkov
committed
Merge branch 'import_externals'
2 parents d5fbddc + 4f70fa3 commit 2a30025

13 files changed

Lines changed: 371 additions & 39 deletions

File tree

src/TypeScriptDefinitionGenerator/Generator/GenerationService.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.ComponentModel.Composition;
77
using System.IO;
8+
using System.Linq;
89
using System.Windows.Threading;
910
using Microsoft.Internal.VisualStudio.PlatformUI;
1011
using TypeScriptDefinitionGenerator.Helpers;
@@ -58,13 +59,23 @@ public static string ConvertToTypeScript(ProjectItem sourceItem)
5859
Options.ReadOptionOverrides(sourceItem);
5960
VSHelpers.WriteOnOutputWindow(string.Format("{0} - Started", sourceItem.Name));
6061
var list = IntellisenseParser.ProcessFile(sourceItem);
62+
// path is needed for relative paths of imports
63+
var sourceItemPath = sourceItem.Properties.Item("FullPath").Value as string;
6164
VSHelpers.WriteOnOutputWindow(string.Format("{0} - Completed", sourceItem.Name));
62-
return IntellisenseWriter.WriteTypeScript(list);
65+
return IntellisenseWriter.WriteTypeScript(list.ToList(), sourceItemPath);
6366
}
6467
catch (Exception ex)
6568
{
6669
VSHelpers.WriteOnOutputWindow(string.Format("{0} - Failure", sourceItem.Name));
67-
Telemetry.TrackException("ParseFailure", ex);
70+
if (ex is ExceptionForUser)
71+
{
72+
// "expected" exception, show to user instead of reporting
73+
VSHelpers.WriteOnOutputWindow(ex.Message);
74+
}
75+
else
76+
{
77+
Telemetry.TrackException("ParseFailure", ex);
78+
}
6879
return null;
6980
}
7081
}

src/TypeScriptDefinitionGenerator/Generator/IntellisenseWriter.cs

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.IO;
34
using System.Linq;
45
using System.Text;
56
using System.Text.RegularExpressions;
@@ -11,7 +12,13 @@ internal static class IntellisenseWriter
1112
{
1213
private static readonly Regex _whitespaceTrimmer = new Regex(@"^\s+|\s+$|\s*[\r\n]+\s*", RegexOptions.Compiled);
1314

14-
public static string WriteTypeScript(IEnumerable<IntellisenseObject> objects)
15+
/// <summary>
16+
/// Generates TypeScript file for given C# class/enum (IntellisenseObject).
17+
/// </summary>
18+
/// <param name="objects">IntellisenseObject of class/enum</param>
19+
/// <param name="sourceItemPath">Path to C# source file</param>
20+
/// <returns>TypeScript file content as string</returns>
21+
public static string WriteTypeScript(IList<IntellisenseObject> objects, string sourceItemPath)
1522
{
1623
var sb = new StringBuilder();
1724
sb.AppendLine("// ------------------------------------------------------------------------------");
@@ -20,57 +27,118 @@ public static string WriteTypeScript(IEnumerable<IntellisenseObject> objects)
2027
sb.AppendLine("// </auto-generated>");
2128
sb.AppendLine("// ------------------------------------------------------------------------------");
2229

30+
string export = !Options.DeclareModule ? "export " : string.Empty;
31+
string prefixModule = Options.DeclareModule ? "\t" : string.Empty;
32+
33+
var sbBody = new StringBuilder();
34+
35+
var neededImports = new List<string>();
36+
2337
foreach (var ns in objects.GroupBy(o => o.Namespace))
2438
{
2539
if (Options.DeclareModule)
2640
{
27-
sb.AppendFormat("declare module {0} {{\r\n", ns.Key);
41+
sbBody.AppendFormat("declare module {0} {{\r\n", ns.Key);
2842
}
2943

30-
string export = !Options.DeclareModule ? "export " : string.Empty;
31-
string prefixModule = Options.DeclareModule ? "\t" : String.Empty;
32-
3344
foreach (IntellisenseObject io in ns)
3445
{
3546
if (!string.IsNullOrEmpty(io.Summary))
36-
sb.Append(prefixModule).AppendLine("/** " + _whitespaceTrimmer.Replace(io.Summary, "") + " */");
47+
sbBody.Append(prefixModule).AppendLine("/** " + _whitespaceTrimmer.Replace(io.Summary, "") + " */");
3748

3849
if (io.IsEnum)
3950
{
4051
string type = "enum ";
41-
sb.Append(prefixModule).Append(export).Append(type).Append(Utility.CamelCaseClassName(io.Name)).Append(" ");
52+
sbBody.Append(prefixModule).Append(export).Append(type).Append(Utility.CamelCaseClassName(io.Name)).Append(" ");
4253

43-
sb.AppendLine("{");
44-
WriteTSEnumDefinition(sb, prefixModule + "\t", io.Properties);
45-
sb.Append(prefixModule).AppendLine("}");
54+
sbBody.AppendLine("{");
55+
WriteTSEnumDefinition(sbBody, prefixModule + "\t", io.Properties);
56+
sbBody.Append(prefixModule).AppendLine("}");
4657
}
4758
else
4859
{
4960
string type = Options.ClassInsteadOfInterface ? "class " : "interface ";
50-
sb.Append(prefixModule).Append(export).Append(type).Append(Utility.CamelCaseClassName(io.Name)).Append(" ");
61+
sbBody.Append(prefixModule).Append(export).Append(type).Append(Utility.CamelCaseClassName(io.Name)).Append(" ");
5162

5263
if (!string.IsNullOrEmpty(io.BaseName))
5364
{
54-
sb.Append("extends ");
65+
sbBody.Append("extends ");
5566

5667
if (!string.IsNullOrEmpty(io.BaseNamespace) && io.BaseNamespace != io.Namespace)
57-
sb.Append(io.BaseNamespace).Append(".");
68+
sbBody.Append(io.BaseNamespace).Append(".");
5869

59-
sb.Append(Utility.CamelCaseClassName(io.BaseName)).Append(" ");
70+
sbBody.Append(Utility.CamelCaseClassName(io.BaseName)).Append(" ");
6071
}
6172

62-
sb.AppendLine("{");
63-
WriteTSInterfaceDefinition(sb, prefixModule + "\t", io.Properties);
64-
sb.Append(prefixModule).AppendLine("}");
73+
sbBody.AppendLine("{");
74+
WriteTSInterfaceDefinition(sbBody, prefixModule + "\t", io.Properties);
75+
sbBody.Append(prefixModule).AppendLine("}");
76+
// Remember client-side references for which we need imports.
77+
// Dictionary are built-in into TS, they need no imports.
78+
neededImports.AddRange(io.Properties.Where(p => p.Type.ClientSideReferenceName != null &&
79+
!p.Type.IsDictionary).Select(p => p.Type.ClientSideReferenceName));
6580
}
6681
}
6782

6883
if (Options.DeclareModule)
6984
{
70-
sb.AppendLine("}");
85+
sbBody.AppendLine("}");
86+
}
87+
}
88+
89+
// if interface, import external interfaces and base classes
90+
if (!Options.DeclareModule)
91+
{
92+
var imports = new List<string>();
93+
94+
var references = objects.SelectMany(o => o.References).Distinct();
95+
foreach (var reference in references)
96+
{
97+
var referencePathRelative = Utility.GetRelativePath(sourceItemPath, reference);
98+
// remove trailing ".ts" which is not expected for TS imports
99+
referencePathRelative = referencePathRelative.Substring(0, referencePathRelative.Length - 3);
100+
// make sure path contains forward slashes which are expected by TS
101+
referencePathRelative = referencePathRelative.Replace(Path.DirectorySeparatorChar, '/');
102+
var referenceName = Utility.RemoveDefaultExtension(Path.GetFileName(reference));
103+
104+
// skipped indirect references
105+
if (!neededImports.Contains(referenceName))
106+
{
107+
continue;
108+
}
109+
110+
sb.AppendLine($"import {{ {referenceName} }} from \"{referencePathRelative}\";");
111+
imports.Add(referenceName);
112+
}
113+
114+
// also import base classes if not yet imported
115+
var baseClasses = objects.Select(o => o.BaseName).Where(b => b != null && !imports.Contains(b)).Distinct();
116+
foreach (var b in baseClasses)
117+
{
118+
var expectedBaseClassPath = Path.Combine(Path.GetDirectoryName(sourceItemPath), b + ".cs");
119+
if (!File.Exists(expectedBaseClassPath))
120+
{
121+
throw new ExceptionForUser($"Could not find base class for {b}. Expected path: {expectedBaseClassPath}");
122+
}
123+
124+
sb.AppendLine($"import {{ {b} }} from \"./{b}.generated\";");
125+
imports.Add(b);
126+
}
127+
128+
var notImportedNeededImports = neededImports.Except(imports).ToList();
129+
if (notImportedNeededImports.Any())
130+
{
131+
var exceptionForDeveloper =
132+
$"Sorry, needed imports missing: {string.Join(", ", notImportedNeededImports)}. " +
133+
$"Make sure file names match contained class/enum name.";
134+
sb.AppendLine($"// {exceptionForDeveloper}");
135+
VSHelpers.WriteOnOutputWindow(exceptionForDeveloper);
136+
//throw new ExceptionForUser(exceptionForDeveloper);
71137
}
72138
}
73139

140+
sb.Append(sbBody);
141+
74142
if (Options.EOLType == EOLType.LF)
75143
sb.Replace("\r\n", "\n");
76144

@@ -88,7 +156,7 @@ private static string CleanEnumInitValue(string value)
88156
if (trimedValue.Length > 0) return trimedValue;
89157
return "0";
90158
}
91-
159+
92160
private static void WriteTypeScriptComment(IntellisenseProperty p, StringBuilder sb, string prefix)
93161
{
94162
if (string.IsNullOrEmpty(p.Summary)) return;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace TypeScriptDefinitionGenerator.Helpers
8+
{
9+
/// <summary>
10+
/// Exception which contains message which can be shown to user.
11+
/// </summary>
12+
class ExceptionForUser : Exception
13+
{
14+
public ExceptionForUser(string message) : base(message) { }
15+
public ExceptionForUser(string message, Exception inner) : base(message, inner) { }
16+
}
17+
}

src/TypeScriptDefinitionGenerator/Helpers/Utility.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ public static string GenerateFileName(string sourceFile)
1010
return Path.ChangeExtension(sourceFile, GetDefaultExtension(Path.GetExtension(sourceFile)));
1111
}
1212

13+
public static string RemoveDefaultExtension(string tsFile)
14+
{
15+
var defaultExt = GetDefaultExtension(string.Empty);
16+
if (!tsFile.EndsWith(defaultExt))
17+
{
18+
throw new System.ArgumentException("File must end with default extension");
19+
}
20+
21+
return tsFile.Substring(0, tsFile.Length - defaultExt.Length);
22+
}
23+
24+
1325
public static string GetDefaultExtension(string originalExt)
1426
{
1527
string declaredExt = Options.DeclareModule ? ".d" : string.Empty;
@@ -57,5 +69,37 @@ private static string CamelCase(string name)
5769
}
5870
return name[0].ToString(CultureInfo.CurrentCulture).ToLower(CultureInfo.CurrentCulture) + name.Substring(1);
5971
}
72+
73+
/// <summary>
74+
/// Get relative path of <paramref name="toPath"/> relative to <paramref name="fromPath"/>.
75+
/// </summary>
76+
/// <param name="fromPath">Base path</param>
77+
/// <param name="toPath">File to be reference relatively</param>
78+
/// <returns></returns>
79+
/// <remarks>Source: https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path#answer-485516</remarks>
80+
public static string GetRelativePath(string fromPath, string toPath)
81+
{
82+
int fromAttr = Directory.Exists(fromPath) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL;
83+
int toAttr = Directory.Exists(toPath) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL;
84+
85+
var path = new System.Text.StringBuilder(260); // MAX_PATH
86+
if (PathRelativePathTo(
87+
path,
88+
fromPath,
89+
fromAttr,
90+
toPath,
91+
toAttr) == 0)
92+
{
93+
throw new System.ArgumentException("Paths must have a common prefix");
94+
}
95+
return path.ToString();
96+
}
97+
98+
private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
99+
private const int FILE_ATTRIBUTE_NORMAL = 0x80;
100+
101+
[System.Runtime.InteropServices.DllImport("shlwapi.dll", SetLastError = true)]
102+
private static extern int PathRelativePathTo(System.Text.StringBuilder pszPath,
103+
string pszFrom, int dwAttrFrom, string pszTo, int dwAttrTo);
60104
}
61105
}

src/TypeScriptDefinitionGenerator/TypeScriptDefinitionGenerator.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<Compile Include="Generator\GenerationService.cs" />
5959
<Compile Include="Generator\DtsGenerator.cs" />
6060
<Compile Include="DtsPackage.cs" />
61+
<Compile Include="Helpers\ExceptionForUser.cs" />
6162
<Compile Include="Helpers\Telemetry.cs" />
6263
<Compile Include="Helpers\Utility.cs" />
6364
<Compile Include="Helpers\VSHelpers.cs" />

tests/ClassLibrary1/ClassLibrary1.csproj

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,24 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<None Update="Some.generated.ts">
9-
<DependentUpon>Some.cs</DependentUpon>
10-
</None>
11-
<None Update="SomeClass.generated.ts">
12-
<DependentUpon>SomeClass.cs</DependentUpon>
13-
</None>
8+
<Compile Update="base\ThirdClass.cs">
9+
<Generator></Generator>
10+
</Compile>
11+
<Compile Update="SecondClass.cs">
12+
<Generator></Generator>
13+
</Compile>
14+
<Compile Update="SomeClass.cs">
15+
<Generator></Generator>
16+
</Compile>
17+
<Compile Update="SomeEnum.cs">
18+
<Generator></Generator>
19+
</Compile>
20+
<Compile Update="SomeSomeClass.cs">
21+
<Generator></Generator>
22+
</Compile>
23+
</ItemGroup>
24+
25+
<ItemGroup>
1426
<None Update="SomeSomeClass.generated.d.ts">
1527
<DependentUpon>SomeSomeClass.cs</DependentUpon>
1628
</None>

tests/ClassLibrary1/SecondClass.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ClassLibrary1
2+
{
3+
internal class SecondClass
4+
{
5+
public int MyProperty { get; set; }
6+
public SomeClass Complex1 { get; set; }
7+
public SomeSomeClass Complex2 { get; set; }
8+
}
9+
}

tests/ClassLibrary1/SomeSomeClass.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace ClassLibrary1
55
{
6-
public class SomeSomeClass: SomeClass
6+
public class SomeSomeClass : SomeClass
77
{
88
public int Inc2 { get; set; }
99
public long Inc3 { get; set; }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ClassLibrary1
2+
{
3+
public class ThirdClass
4+
{
5+
public int MyProperty { get; set; }
6+
public SomeClass Complex1 { get; set; }
7+
public SomeSomeClass Complex2 { get; set; }
8+
}
9+
}

0 commit comments

Comments
 (0)