diff --git a/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator.UnitTests/AssemblyGeneratorTests.cs b/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator.UnitTests/AssemblyGeneratorTests.cs index 2c18b5dd07..03f80b066c 100644 --- a/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator.UnitTests/AssemblyGeneratorTests.cs +++ b/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator.UnitTests/AssemblyGeneratorTests.cs @@ -29,6 +29,17 @@ static string GetProjectFilePath(string dotnetPackage, string dotnetAssembly) return $"{Path.Combine(GetPackageOutputRoot(dotnetPackage), dotnetAssembly)}.csproj"; } + static string GetAnchorFilePath(string dotnetPackage, string dotnetNamespace) + { + string path = GetPackageOutputRoot(dotnetPackage); + + foreach (string token in dotnetNamespace.Split('.')) { + path = Path.Combine(path, token); + } + + return Path.Combine(path, "Internal", "DependencyResolution", "Anchor.cs"); + } + static string GetTypeFilePath(string dotnetPackage, string dotnetNamespace, string dotnetType) { string directory = Path.Combine(GetPackageOutputRoot(dotnetPackage), Path.Combine(dotnetNamespace.Split('.'))); @@ -352,6 +363,115 @@ public void CreatesProjectFileWithDependencies() file.Received().WriteAllText(projectFilePath, Arg.Do(actual => PlatformIndependentEqual(expected, actual))); } + [Fact(DisplayName = Prefix + nameof(CreatesAnchorFile))] + public void CreatesAnchorFile() + { + string json = +@"{ + ""name"": ""jsii$aws_cdk$"", + ""description"": """", + ""homepage"": """", + ""repository"": { + ""type"": """", + ""url"": """" + }, + ""author"": { + ""name"": """", + ""roles"": [] + }, + ""fingerprint"": """", + ""license"": """", + ""targets"": { + ""dotnet"": { + ""namespace"": ""Aws.CdkNamespace"", + ""packageId"": ""Aws.CdkPackageId"" + } + }, + ""version"": ""1.2.3"", + ""types"": {}, + ""dependencies"": { + ""jsii$aws_cdk_cx_api$"": { + ""package"": ""aws-cdk-cx-api"", + ""version"": """", + ""targets"": { + ""dotnet"": { + ""namespace"": ""Aws.Cdk.CxApi"", + ""packageId"": ""Aws.Cdk.CxApi"" + } + } + } + } +}"; + string cxJson = +@"{ + ""name"": ""jsii$aws_cdk_cx_api$"", + ""description"": """", + ""homepage"": """", + ""repository"": { + ""type"": """", + ""url"": """" + }, + ""author"": { + ""name"": """", + ""roles"": [] + }, + ""fingerprint"": """", + ""license"": """", + ""version"": """", + ""targets"": { + ""dotnet"": { + ""namespace"": ""Aws.Cdk.CxApiNamespace"", + ""packageId"": ""Aws.Cdk.CxApiPackageId"" + } + }, + ""types"": {} +}"; + + string jsonPath = GetJsonPath("aws-cdk"); + string cxJsonPath = Path.Combine(Path.GetDirectoryName(jsonPath), "node_modules", "jsii$aws_cdk_cx_api$"); + string anchorFilePath = GetAnchorFilePath("Aws.CdkPackageId", "Aws.CdkNamespace"); + + IFile file = Substitute.For(); + file.ReadAllText(jsonPath).Returns(json); + file.ReadAllText(Path.Combine(cxJsonPath, ".jsii")).Returns(cxJson); + + IDirectory directory = Substitute.For(); + directory.Exists(cxJsonPath).Returns(true); + + IFileSystem fileSystem = Substitute.For(); + fileSystem.Directory.Returns(directory); + fileSystem.File.Returns(file); + + Symbols.MapTypeToPackage("aws-cdk", "Aws.CdkPackageId"); + Symbols.MapTypeToPackage("aws-cdk-cx-api", "Aws.Cdk.CxApiNamespace"); + Symbols.MapAssemblyName("jsii$aws_cdk_cx_api$", "Aws.Cdk.CxApiPackageId"); + + AssemblyGenerator generator = new AssemblyGenerator + ( + OutputRoot, + fileSystem + ); + generator.Generate + ( + Path.Combine(InputRoot, "aws-cdk", "dist", Constants.SPEC_FILE_NAME), + Path.Combine(InputRoot, "aws-cdk", "aws-cdk-1.2.3.4.tgz"), + Symbols + ); + + string expected = +@"namespace Aws.CdkNamespace.Internal.DependencyResolution +{ + public class Anchor + { + public Anchor() + { + new Aws.Cdk.CxApiNamespace.Internal.DependencyResolution.Anchor(); + } + } +}"; + file.Received().WriteAllText(anchorFilePath, Arg.Do(actual => PlatformIndependentEqual(expected, actual))); + } + [Fact(DisplayName = Prefix + nameof(CreatesAssemblyInfo))] public void CreatesAssemblyInfo() { diff --git a/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator/AssemblyGenerator.cs b/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator/AssemblyGenerator.cs index c805300141..d05786067c 100644 --- a/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator/AssemblyGenerator.cs +++ b/packages/jsii-dotnet-generator/src/Amazon.JSII.Generator/AssemblyGenerator.cs @@ -25,8 +25,7 @@ public class AssemblyGenerator readonly string _outputRoot; readonly IFileSystem _fileSystem; - public AssemblyGenerator - ( + public AssemblyGenerator( string outputRoot, IFileSystem fileSystem = null ) @@ -102,6 +101,7 @@ void Save(string packageOutputRoot, ISymbolMap symbols, Assembly assembly, strin SaveProjectFile(); SaveAssemblyInfo(assembly.Name, assembly.Version, tarballFileName); + SaveDependencyAnchorFile(); foreach (Type type in assembly.Types?.Values ?? Enumerable.Empty()) { @@ -185,6 +185,64 @@ IEnumerable> GetDependenciesCore(Dependency } } + void SaveDependencyAnchorFile() + { + string anchorNamespace = $"{assembly.GetNativeNamespace()}.Internal.DependencyResolution"; + var syntaxTree = SF.SyntaxTree( + SF.CompilationUnit( + SF.List(), + SF.List(), + SF.List(), + SF.List(new[] { + SF.NamespaceDeclaration( + SF.IdentifierName(anchorNamespace), + SF.List(), + SF.List(), + SF.List(new MemberDeclarationSyntax[] { GenerateDependencyAnchor() }) + ) + }) + ).NormalizeWhitespace(elasticTrivia: true) + ); + + string directory = GetNamespaceDirectory(packageOutputRoot, @anchorNamespace); + SaveSyntaxTree(directory, "Anchor.cs", syntaxTree); + + ClassDeclarationSyntax GenerateDependencyAnchor() { + return SF.ClassDeclaration( + SF.List(), + SF.TokenList(SF.Token(SyntaxKind.PublicKeyword)), + SF.Identifier("Anchor"), + null, + null, + SF.List(), + SF.List(new MemberDeclarationSyntax[] { + SF.ConstructorDeclaration( + SF.List(), + SF.TokenList(SF.Token(SyntaxKind.PublicKeyword)), + SF.Identifier("Anchor"), + SF.ParameterList(SF.SeparatedList()), + null, + SF.Block(SF.List(GenerateAnchorReferences())), + null + ) + }) + ); + + IEnumerable GenerateAnchorReferences() + { + return assembly.Dependencies?.Keys + .Select(k => assembly.GetNativeNamespace(k)) + .Select(n => $"{n}.Internal.DependencyResolution.Anchor") + .Select(t => SF.ExpressionStatement(SF.ObjectCreationExpression( + SF.Token(SyntaxKind.NewKeyword), + SF.ParseTypeName(t), + SF.ArgumentList(SF.SeparatedList()), + null + ))) ?? Enumerable.Empty(); + } + } + } + void SaveAssemblyInfo(string name, string version, string tarball) { SyntaxTree assemblyInfo = SF.SyntaxTree( @@ -217,14 +275,8 @@ void SaveAssemblyInfo(string name, string version, string tarball) void SaveType(Type type) { - string packageName = Path.GetFileName(packageOutputRoot); string @namespace = symbols.GetNamespace(type); - if (@namespace.StartsWith(packageName)) - { - @namespace = @namespace.Substring(packageName.Length).TrimStart('.'); - } - - string directory = Path.Combine(packageOutputRoot, Path.Combine(@namespace.Split('.'))); + string directory = GetNamespaceDirectory(packageOutputRoot, @namespace); switch (type.Kind) { @@ -252,17 +304,37 @@ void SaveType(Type type) void SaveTypeFile(string filename, SyntaxTree syntaxTree) { - if (!_fileSystem.Directory.Exists(directory)) - { - _fileSystem.Directory.CreateDirectory(directory); - } + SaveSyntaxTree(directory, filename, syntaxTree); + } + } - _fileSystem.File.WriteAllText( - Path.Combine(directory, filename), - syntaxTree.ToString() - ); + void SaveSyntaxTree(string directory, string filename, SyntaxTree syntaxTree) + { + if (!_fileSystem.Directory.Exists(directory)) + { + _fileSystem.Directory.CreateDirectory(directory); } + + _fileSystem.File.WriteAllText( + Path.Combine(directory, filename), + syntaxTree.ToString() + ); + } + } + + string GetNamespaceDirectory(string packageOutputRoot, string @namespace) + { + string packageName = Path.GetFileName(packageOutputRoot); + + // packageOutputRoot is typically a directory like My.Root.Namespace/ + // We don't want to create redundant directories, i.e. + // My.Root.Namespace/My/Root/Namespace/Sub/Namespace. We just + // want My.Root.Namespace/Sub/Namespace. + if (@namespace.StartsWith(packageName)) { + @namespace = @namespace.Substring(packageName.Length).TrimStart('.'); } + + return Path.Combine(packageOutputRoot, Path.Combine(@namespace.Split('.'))); } } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-base/dotnet/Amazon.JSII.Tests.CalculatorPackageId.BasePackageId/Amazon/JSII/Tests/CalculatorNamespace/BaseNamespace/Internal/DependencyResolution/Anchor.cs b/packages/jsii-pacmak/test/expected.jsii-calc-base/dotnet/Amazon.JSII.Tests.CalculatorPackageId.BasePackageId/Amazon/JSII/Tests/CalculatorNamespace/BaseNamespace/Internal/DependencyResolution/Anchor.cs new file mode 100644 index 0000000000..c11dfffe04 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc-base/dotnet/Amazon.JSII.Tests.CalculatorPackageId.BasePackageId/Amazon/JSII/Tests/CalculatorNamespace/BaseNamespace/Internal/DependencyResolution/Anchor.cs @@ -0,0 +1,10 @@ +namespace Amazon.JSII.Tests.CalculatorNamespace.BaseNamespace.Internal.DependencyResolution +{ + public class Anchor + { + public Anchor() + { + new Amazon.JSII.Tests.CalculatorNamespace.BaseOfBaseNamespace.Internal.DependencyResolution.Anchor(); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-lib/dotnet/Amazon.JSII.Tests.CalculatorPackageId.LibPackageId/Amazon/JSII/Tests/CalculatorNamespace/LibNamespace/Internal/DependencyResolution/Anchor.cs b/packages/jsii-pacmak/test/expected.jsii-calc-lib/dotnet/Amazon.JSII.Tests.CalculatorPackageId.LibPackageId/Amazon/JSII/Tests/CalculatorNamespace/LibNamespace/Internal/DependencyResolution/Anchor.cs new file mode 100644 index 0000000000..a12334cc09 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc-lib/dotnet/Amazon.JSII.Tests.CalculatorPackageId.LibPackageId/Amazon/JSII/Tests/CalculatorNamespace/LibNamespace/Internal/DependencyResolution/Anchor.cs @@ -0,0 +1,10 @@ +namespace Amazon.JSII.Tests.CalculatorNamespace.LibNamespace.Internal.DependencyResolution +{ + public class Anchor + { + public Anchor() + { + new Amazon.JSII.Tests.CalculatorNamespace.BaseNamespace.Internal.DependencyResolution.Anchor(); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Internal/DependencyResolution/Anchor.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Internal/DependencyResolution/Anchor.cs new file mode 100644 index 0000000000..af4f74d979 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Internal/DependencyResolution/Anchor.cs @@ -0,0 +1,11 @@ +namespace Amazon.JSII.Tests.CalculatorNamespace.Internal.DependencyResolution +{ + public class Anchor + { + public Anchor() + { + new Amazon.JSII.Tests.CalculatorNamespace.BaseNamespace.Internal.DependencyResolution.Anchor(); + new Amazon.JSII.Tests.CalculatorNamespace.LibNamespace.Internal.DependencyResolution.Anchor(); + } + } +} \ No newline at end of file diff --git a/packages/jsii-runtime/package-lock.json b/packages/jsii-runtime/package-lock.json index 2ff60df500..1efa260a7e 100644 --- a/packages/jsii-runtime/package-lock.json +++ b/packages/jsii-runtime/package-lock.json @@ -4630,7 +4630,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4648,11 +4649,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4665,15 +4668,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4776,7 +4782,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4786,6 +4793,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4798,17 +4806,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4825,6 +4836,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4897,7 +4909,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4907,6 +4920,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4982,7 +4996,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5012,6 +5027,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5029,6 +5045,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5067,11 +5084,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } },