diff --git a/Razor.sln b/Razor.sln index 600604db12..9c88e5201d 100644 --- a/Razor.sln +++ b/Razor.sln @@ -78,9 +78,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Mac. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test", "test\Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test\Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test.csproj", "{B8A3E4CA-D54A-441F-A3BF-E00F060CA042}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Design", "src\Microsoft.AspNetCore.Razor.Design\Microsoft.AspNetCore.Razor.Design.csproj", "{5257B25D-330A-4DCF-ACED-B4709CFBF916}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Design", "src\Microsoft.AspNetCore.Razor.Design\Microsoft.AspNetCore.Razor.Design.csproj", "{5257B25D-330A-4DCF-ACED-B4709CFBF916}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Design.Test", "test\Microsoft.AspNetCore.Razor.Design.Test\Microsoft.AspNetCore.Razor.Design.Test.csproj", "{1D90F276-E1CA-4FDF-A173-EB889E7D3150}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Design.Test", "test\Microsoft.AspNetCore.Razor.Design.Test\Microsoft.AspNetCore.Razor.Design.Test.csproj", "{1D90F276-E1CA-4FDF-A173-EB889E7D3150}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.GenerateTool", "src\Microsoft.AspNetCore.Razor.GenerateTool\Microsoft.AspNetCore.Razor.GenerateTool.csproj", "{8052F088-1204-427B-BB0D-BE3D3BA6811B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Tasks", "src\Microsoft.AspNetCore.Razor.Tasks\Microsoft.AspNetCore.Razor.Tasks.csproj", "{043B9497-C0BA-4770-9210-4456D2F81CE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.TagHelperTool", "src\Microsoft.AspNetCore.Razor.TagHelperTool\Microsoft.AspNetCore.Razor.TagHelperTool.csproj", "{AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -336,6 +342,30 @@ Global {1D90F276-E1CA-4FDF-A173-EB889E7D3150}.Release|Any CPU.Build.0 = Release|Any CPU {1D90F276-E1CA-4FDF-A173-EB889E7D3150}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {1D90F276-E1CA-4FDF-A173-EB889E7D3150}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.Release|Any CPU.Build.0 = Release|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {8052F088-1204-427B-BB0D-BE3D3BA6811B}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.Release|Any CPU.Build.0 = Release|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {043B9497-C0BA-4770-9210-4456D2F81CE0}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.Release|Any CPU.Build.0 = Release|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -372,6 +402,9 @@ Global {B8A3E4CA-D54A-441F-A3BF-E00F060CA042} = {92463391-81BE-462B-AC3C-78C6C760741F} {5257B25D-330A-4DCF-ACED-B4709CFBF916} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {1D90F276-E1CA-4FDF-A173-EB889E7D3150} = {92463391-81BE-462B-AC3C-78C6C760741F} + {8052F088-1204-427B-BB0D-BE3D3BA6811B} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} + {043B9497-C0BA-4770-9210-4456D2F81CE0} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} + {AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26} diff --git a/build/dependencies.props b/build/dependencies.props index 7446297713..d83f087a60 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -7,8 +7,11 @@ 2.1.0-preview1-15626 2.1.0-preview1-27807 2.1.0-preview1-27807 + 15.3.409 + 15.3.409 2.3.1 - 2.3.1 + 2.3.1 + 2.1.0-preview1-27807 2.1.0-preview1-27807 2.1.0-preview2-25711-01 2.1.0-preview1-27807 @@ -50,5 +53,6 @@ 2.3.1 2.3.1 + diff --git a/src/Microsoft.AspNetCore.Razor.GenerateTool/Application.cs b/src/Microsoft.AspNetCore.Razor.GenerateTool/Application.cs new file mode 100644 index 0000000000..aa7811a230 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.GenerateTool/Application.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Razor.GenerateTool +{ + internal class Application : CommandLineApplication + { + public Application() + { + Name = "razor-generatetool"; + FullName = "Microsoft ASP.NET Core Razor Generate Tool"; + Description = "Generates C# Code from Razor source files."; + ShortVersionGetter = GetInformationalVersion; + + HelpOption("-?|-h|--help"); + + ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); + OutputDirectory = Option("-o", "output directory", CommandOptionType.SingleValue); + TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); + Sources = Argument("sources", ".cshtml files to compile", multipleValues: true); + + new RunCommand().Configure(this); + } + + public CommandArgument Sources { get; } + + public CommandOption OutputDirectory { get; } + + public CommandOption ProjectDirectory { get; } + + public CommandOption TagHelperManifest { get; } + + public new int Execute(params string[] args) + { + try + { + return base.Execute(ExpandResponseFiles(args)); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + Error.WriteLine(ex.InnerException.Message); + Error.WriteLine(ex.InnerException.StackTrace); + return 1; + } + catch (Exception ex) + { + Error.WriteLine(ex.Message); + Error.WriteLine(ex.StackTrace); + return 1; + } + } + + private string GetInformationalVersion() + { + var assembly = typeof(Application).GetTypeInfo().Assembly; + var attribute = assembly.GetCustomAttribute(); + return attribute.InformationalVersion; + } + + private static string[] ExpandResponseFiles(string[] args) + { + var expandedArgs = new List(); + foreach (var arg in args) + { + if (!arg.StartsWith("@", StringComparison.Ordinal)) + { + expandedArgs.Add(arg); + } + else + { + var fileName = arg.Substring(1); + expandedArgs.AddRange(File.ReadLines(fileName)); + } + } + + return expandedArgs.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.GenerateTool/DebugMode.cs b/src/Microsoft.AspNetCore.Razor.GenerateTool/DebugMode.cs new file mode 100644 index 0000000000..0de1f0b078 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.GenerateTool/DebugMode.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Microsoft.AspNetCore.Razor.GenerateTool +{ + internal static class DebugMode + { + public static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + + while (!Debugger.IsAttached) + { + Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); + Thread.Sleep(TimeSpan.FromSeconds(3)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.GenerateTool/Microsoft.AspNetCore.Razor.GenerateTool.csproj b/src/Microsoft.AspNetCore.Razor.GenerateTool/Microsoft.AspNetCore.Razor.GenerateTool.csproj new file mode 100644 index 0000000000..e6286da6c8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.GenerateTool/Microsoft.AspNetCore.Razor.GenerateTool.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.0 + Exe + + + false + false + + + + + Shared\RazorDiagnosticJsonConverter.cs + + + Shared\TagHelperDescriptorJsonConverter.cs + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Razor.GenerateTool/Program.cs b/src/Microsoft.AspNetCore.Razor.GenerateTool/Program.cs new file mode 100644 index 0000000000..6a433eff5e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.GenerateTool/Program.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.GenerateTool +{ + internal static class Program + { + public static int Main(string[] args) + { + DebugMode.HandleDebugSwitch(ref args); + + var application = new Application(); + return application.Execute(args); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs b/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs new file mode 100644 index 0000000000..8236213ce2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.LanguageServices.Razor; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.GenerateTool +{ + internal class RunCommand + { + public void Configure(Application application) + { + application.OnExecute(() => Execute(application)); + } + + private int Execute(Application application) + { + if (!ValidateArguments(application)) + { + application.ShowHelp(); + return 1; + } + + return ExecuteCore( + projectDirectory: application.ProjectDirectory.Value() ?? Environment.CurrentDirectory, + outputDirectory: application.OutputDirectory.Value(), + tagHelperManifest: application.TagHelperManifest.Value(), + sources: application.Sources.Values.ToArray()); + } + + private int ExecuteCore(string projectDirectory, string outputDirectory, string tagHelperManifest, string[] sources) + { + var tagHelpers = GetTagHelpers(tagHelperManifest); + + var engine = RazorEngine.Create(b => + { + RazorExtensions.Register(b); + + b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); + }); + + var templateEngine = new MvcRazorTemplateEngine(engine, RazorProject.Create(projectDirectory)); + + var sourceItems = GetRazorFiles(projectDirectory, sources); + var results = GenerateCode(templateEngine, sourceItems); + + var success = true; + + foreach (var result in results) + { + if (result.CSharpDocument.Diagnostics.Count > 0) + { + success = false; + foreach (var error in result.CSharpDocument.Diagnostics) + { + Console.Error.WriteLine(error.GetMessage()); + } + } + + var outputFilePath = Path.Combine(outputDirectory, Path.ChangeExtension(result.ViewFileInfo.ViewEnginePath.Substring(1), ".cs")); + File.WriteAllText(outputFilePath, result.CSharpDocument.GeneratedCode); + } + + return success ? 0 : -1; + } + + private IReadOnlyList GetTagHelpers(string tagHelperManifest) + { + if (!File.Exists(tagHelperManifest)) + { + return Array.Empty(); + } + + using (var stream = File.OpenRead(tagHelperManifest)) + { + var reader = new JsonTextReader(new StreamReader(stream)); + + var serializer = new JsonSerializer(); + serializer.Converters.Add(new RazorDiagnosticJsonConverter()); + serializer.Converters.Add(new TagHelperDescriptorJsonConverter()); + + return serializer.Deserialize>(reader); + } + } + + private List GetRazorFiles(string projectDirectory, string[] sources) + { + var trimLength = projectDirectory.EndsWith("/") ? projectDirectory.Length - 1 : projectDirectory.Length; + + var items = new List(sources.Length); + for (var i = 0; i < sources.Length; i++) + { + var fullPath = Path.Combine(projectDirectory, sources[i]); + if (fullPath.StartsWith(projectDirectory, StringComparison.OrdinalIgnoreCase)) + { + var viewEnginePath = fullPath.Substring(trimLength).Replace('\\', '/'); + items.Add(new SourceItem(fullPath, viewEnginePath)); + } + } + + return items; + } + + private OutputItem[] GenerateCode(RazorTemplateEngine templateEngine, IReadOnlyList sources) + { + var outputs = new OutputItem[sources.Count]; + Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = 4 }, i => + { + var source = sources[i]; + + var csharpDocument = templateEngine.GenerateCode(source.ViewEnginePath); + outputs[i] = new OutputItem(source, csharpDocument); + }); + + return outputs; + } + + private bool ValidateArguments(Application application) + { + if (string.IsNullOrEmpty(application.OutputDirectory.Value())) + { + application.Error.WriteLine($"{application.OutputDirectory.ValueName} not specified."); + return false; + } + + if (application.Sources.Values.Count == 0) + { + application.Error.WriteLine($"{application.Sources.Name} should have at least one value."); + return false; + } + + return true; + } + + private struct OutputItem + { + public OutputItem( + SourceItem viewFileInfo, + RazorCSharpDocument cSharpDocument) + { + ViewFileInfo = viewFileInfo; + CSharpDocument = cSharpDocument; + } + + public SourceItem ViewFileInfo { get; } + + public RazorCSharpDocument CSharpDocument { get; } + } + + private struct SourceItem + { + public SourceItem(string fullPath, string viewEnginePath) + { + FullPath = fullPath; + ViewEnginePath = viewEnginePath; + } + + public string FullPath { get; } + + public string ViewEnginePath { get; } + + public Stream CreateReadStream() + { + // We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer + // 0 causes constructor to throw + var bufferSize = 1; + return new FileStream( + FullPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + } + } + + private class StaticTagHelperFeature : ITagHelperFeature + { + public RazorEngine Engine { get; set; } + + public IReadOnlyList TagHelpers { get; set; } + + public IReadOnlyList GetDescriptors() => TagHelpers; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs index 2e1dafb7a8..c97fe625b4 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs @@ -4,7 +4,9 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.GenerateTool, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Language.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.TagHelperTool, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Test.Common, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs new file mode 100644 index 0000000000..0fbcf34602 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Razor.TagHelperTool +{ + internal class Application : CommandLineApplication + { + public Application() + { + Name = "razor-taghelpertool"; + FullName = "Microsoft ASP.NET Core Razor Tag Helper tool"; + Description = "Discovers tag helpers in an assembly graph."; + ShortVersionGetter = GetInformationalVersion; + + HelpOption("-?|-h|--help"); + + TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); + Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true); + + new RunCommand().Configure(this); + } + + public CommandArgument Assemblies { get; } + + public CommandOption TagHelperManifest { get; } + + public new int Execute(params string[] args) + { + try + { + return base.Execute(ExpandResponseFiles(args)); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + Error.WriteLine(ex.InnerException.Message); + Error.WriteLine(ex.InnerException.StackTrace); + return 1; + } + catch (Exception ex) + { + Error.WriteLine(ex.Message); + Error.WriteLine(ex.StackTrace); + return 1; + } + } + + private string GetInformationalVersion() + { + var assembly = typeof(Application).GetTypeInfo().Assembly; + var attribute = assembly.GetCustomAttribute(); + return attribute.InformationalVersion; + } + + private static string[] ExpandResponseFiles(string[] args) + { + var expandedArgs = new List(); + foreach (var arg in args) + { + if (!arg.StartsWith("@", StringComparison.Ordinal)) + { + expandedArgs.Add(arg); + } + else + { + var fileName = arg.Substring(1); + expandedArgs.AddRange(File.ReadLines(fileName)); + } + } + + return expandedArgs.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/DebugMode.cs b/src/Microsoft.AspNetCore.Razor.TagHelperTool/DebugMode.cs new file mode 100644 index 0000000000..38799a9791 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/DebugMode.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Microsoft.AspNetCore.Razor.TagHelperTool +{ + internal static class DebugMode + { + public static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + + while (!Debugger.IsAttached) + { + Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); + Thread.Sleep(TimeSpan.FromSeconds(3)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/Microsoft.AspNetCore.Razor.TagHelperTool.csproj b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Microsoft.AspNetCore.Razor.TagHelperTool.csproj new file mode 100644 index 0000000000..608a493290 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Microsoft.AspNetCore.Razor.TagHelperTool.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.0 + Exe + + + false + false + + + + + Shared\RazorDiagnosticJsonConverter.cs + + + Shared\TagHelperDescriptorJsonConverter.cs + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/Program.cs b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Program.cs new file mode 100644 index 0000000000..29afc1f466 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Program.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.TagHelperTool +{ + internal static class Program + { + public static int Main(string[] args) + { + DebugMode.HandleDebugSwitch(ref args); + + var application = new Application(); + return application.Execute(args); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs b/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs new file mode 100644 index 0000000000..9c8720b073 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.LanguageServices.Razor; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.TagHelperTool +{ + internal class RunCommand + { + public void Configure(Application application) + { + application.OnExecute(() => Execute(application)); + } + + private int Execute(Application application) + { + if (!ValidateArguments(application)) + { + application.ShowHelp(); + return 1; + } + + return ExecuteCore( + outputFilePath: application.TagHelperManifest.Value(), + assemblies: application.Assemblies.Values.ToArray()); + } + + private int ExecuteCore(string outputFilePath, string[] assemblies) + { + var metadataReferences = new MetadataReference[assemblies.Length]; + for (var i = 0; i < assemblies.Length; i++) + { + metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]); + } + + var engine = RazorEngine.Create((b) => + { + RazorExtensions.Register(b); + + b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences }); + b.Features.Add(new CompilationTagHelperFeature()); + }); + + var feature = engine.Features.OfType().Single(); + var tagHelpers = feature.GetDescriptors(); + + using (var stream = new MemoryStream()) + { + Serialize(stream, tagHelpers); + + stream.Position = 0L; + + var newHash = Hash(stream); + var existingHash = Hash(outputFilePath); + + if (!HashesEqual(newHash, existingHash)) + { + stream.Position = 0; + using (var output = File.OpenWrite(outputFilePath)) + { + stream.CopyTo(output); + } + } + } + + return 0; + } + + private static byte[] Hash(string path) + { + if (!File.Exists(path)) + { + return Array.Empty(); + } + + using (var stream = File.OpenRead(path)) + { + return Hash(stream); + } + } + + private static byte[] Hash(Stream stream) + { + using (var sha = SHA256.Create()) + { + sha.ComputeHash(stream); + return sha.Hash; + } + } + + private bool HashesEqual(byte[] x, byte[] y) + { + if (x.Length != y.Length) + { + return false; + } + + for (var i = 0; i < x.Length; i++) + { + if (x[i] != y[i]) + { + return false; + } + } + + return true; + } + + private static void Serialize(Stream stream, IReadOnlyList tagHelpers) + { + using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true)) + { + var serializer = new JsonSerializer(); + serializer.Converters.Add(new TagHelperDescriptorJsonConverter()); + serializer.Converters.Add(new RazorDiagnosticJsonConverter()); + + serializer.Serialize(writer, tagHelpers); + } + } + + private bool ValidateArguments(Application application) + { + if (string.IsNullOrEmpty(application.TagHelperManifest.Value())) + { + application.Error.WriteLine($"{application.TagHelperManifest.ValueName} not specified."); + return false; + } + + if (application.Assemblies.Values.Count == 0) + { + application.Error.WriteLine($"{application.Assemblies.Name} should have at least one value."); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs b/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs new file mode 100644 index 0000000000..191035d42a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public abstract class DotNetToolTask : ToolTask + { + public bool Debug { get; set; } + + public bool DebugTool { get; set; } + + [Required] + public string ToolAssembly { get; set; } + + protected override string ToolName => "dotnet"; + + // If we're debugging then make all of the stdout gets logged in MSBuild + protected override MessageImportance StandardOutputLoggingImportance => DebugTool ? MessageImportance.High : base.StandardOutputLoggingImportance; + + protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High; + + // use PATH to find dotnet + protected override string GenerateFullPathToTool() => ToolExe; + + protected override string GenerateCommandLineCommands() + { + return $"exec \"{ToolAssembly}\"" + (DebugTool ? " --debug" : ""); + } + + protected override string GetResponseFileSwitch(string responseFilePath) + { + return "@\"" + responseFilePath + "\""; + } + + protected abstract override string GenerateResponseFileCommands(); + + public override bool Execute() + { + if (Debug) + { + while (!Debugger.IsAttached) + { + Log.LogMessage(MessageImportance.High, "Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); + Thread.Sleep(TimeSpan.FromSeconds(3)); + } + } + + return base.Execute(); + } + + protected override void LogToolCommand(string message) + { + if (Debug) + { + Log.LogMessage(MessageImportance.High, message); + } + else + { + base.LogToolCommand(message); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj b/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj new file mode 100644 index 0000000000..c0f4027de2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj @@ -0,0 +1,17 @@ + + + + + net46;netstandard2.0 + + + false + false + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs new file mode 100644 index 0000000000..e485252587 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class RazorGenerate : DotNetToolTask + { + [Required] + public string[] Sources { get; set; } + + [Required] + public string ProjectRoot { get; set; } + + [Required] + public string OutputPath { get; set; } + + [Required] + public string TagHelperManifest { get; set; } + + protected override string GenerateResponseFileCommands() + { + var builder = new StringBuilder(); + for (var i = 0; i < Sources.Length; i++) + { + builder.AppendLine(Sources[i]); + } + + builder.AppendLine("-p"); + builder.AppendLine(ProjectRoot); + + builder.AppendLine("-o"); + builder.AppendLine(OutputPath); + + builder.AppendLine("-t"); + builder.AppendLine(TagHelperManifest); + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs new file mode 100644 index 0000000000..b8a49a0ca9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using System.IO; +using System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class RazorTagHelper : DotNetToolTask + { + [Required] + public string[] Assemblies { get; set; } + + [Output] + [Required] + public string TagHelperManifest { get; set; } + + protected override bool SkipTaskExecution() + { + if (Assemblies.Length == 0) + { + // If for some reason there are no assemblies, we have nothing to do. Let's just + // skip running the tool. + File.WriteAllText(TagHelperManifest, "{ }"); + return true; + } + + return false; + } + + protected override string GenerateResponseFileCommands() + { + var builder = new StringBuilder(); + for (var i = 0; i < Assemblies.Length; i++) + { + builder.AppendLine(Assemblies[i]); + } + + builder.AppendLine("-o"); + builder.AppendLine(TagHelperManifest); + + return builder.ToString(); + } + } +}