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();
+ }
+ }
+}