diff --git a/Razor.sln b/Razor.sln index c371903ccd..354061090e 100644 --- a/Razor.sln +++ b/Razor.sln @@ -19,7 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution NuGet.config = NuGet.config EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorPageGenerator", "src\RazorPageGenerator\RazorPageGenerator.csproj", "{7BE58880-36AD-4CD5-9E16-2A5AFEA790EF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorPageGenerator", "src\RazorPageGenerator\RazorPageGenerator.csproj", "{7BE58880-36AD-4CD5-9E16-2A5AFEA790EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Evolution", "src\Microsoft.AspNetCore.Razor.Evolution\Microsoft.AspNetCore.Razor.Evolution.csproj", "{932F3C9C-A6C0-40D3-BA50-9309886242FC}" EndProject diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs index 9d6904244d..c7d811488c 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { @@ -57,6 +58,48 @@ namespace Microsoft.AspNetCore.Razor.Evolution return builder; } + public static IRazorEngineBuilder SetClassName(this IRazorEngineBuilder builder, string className) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + ConfigureClass(builder, (document, @class) => @class.Name = className); + return builder; + } + + public static IRazorEngineBuilder ConfigureClass( + this IRazorEngineBuilder builder, + Action configureClass) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configureClass == null) + { + throw new ArgumentNullException(nameof(configureClass)); + } + + var configurationFeature = GetDefaultDocumentClassifierPassFeature(builder); + configurationFeature.ConfigureClass.Add(configureClass); + return builder; + } + + public static IRazorEngineBuilder SetNamespace(this IRazorEngineBuilder builder, string namespaceName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var configurationFeature = GetDefaultDocumentClassifierPassFeature(builder); + configurationFeature.ConfigureNamespace.Add((document, @namespace) => @namespace.Content = namespaceName); + return builder; + } + private static IRazorDirectiveFeature GetDirectiveFeature(IRazorEngineBuilder builder) { var directiveFeature = builder.Features.OfType().FirstOrDefault(); diff --git a/src/RazorPageGenerator/Program.cs b/src/RazorPageGenerator/Program.cs index 02210e1352..dbb44c30a2 100644 --- a/src/RazorPageGenerator/Program.cs +++ b/src/RazorPageGenerator/Program.cs @@ -2,10 +2,11 @@ // 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 Microsoft.AspNetCore.Razor; -using Microsoft.AspNetCore.Razor.CodeGenerators; +using System.Text; +using Microsoft.AspNetCore.Razor.Evolution; namespace RazorPageGenerator { @@ -23,7 +24,20 @@ namespace RazorPageGenerator var rootNamespace = args[0]; var targetProjectDirectory = Directory.GetCurrentDirectory(); + var razorEngine = RazorEngine.Create(builder => + { + builder + .SetNamespace(rootNamespace) + .SetBaseType("Microsoft.Extensions.RazorViews.BaseView") + .ConfigureClass((document, @class) => + { + @class.Name = Path.GetFileNameWithoutExtension(document.Source.Filename); + @class.AccessModifier = "internal"; + }); + builder.Features.Add(new RemovePragamaChecksumFeature()); + + }); var viewDirectories = Directory.EnumerateDirectories(targetProjectDirectory, "Views", SearchOption.AllDirectories); @@ -32,8 +46,11 @@ namespace RazorPageGenerator { Console.WriteLine(); Console.WriteLine(" Generating code files for views in {0}", viewDir); + var razorProject = new FileSystemRazorProject(viewDir); + var templateEngine = new RazorTemplateEngine(razorEngine, razorProject); - var cshtmlFiles = Directory.EnumerateFiles(viewDir, "*.cshtml"); + + var cshtmlFiles = razorProject.EnumerateItems(""); if (!cshtmlFiles.Any()) { @@ -41,10 +58,10 @@ namespace RazorPageGenerator continue; } - foreach (var fileName in cshtmlFiles) + foreach (var item in cshtmlFiles) { - Console.WriteLine(" Generating code file for view {0}...", Path.GetFileName(fileName)); - GenerateCodeFile(fileName, rootNamespace); + Console.WriteLine(" Generating code file for view {0}...", item.Filename); + GenerateCodeFile(templateEngine, item); Console.WriteLine(" Done!"); fileCount++; } @@ -55,66 +72,104 @@ namespace RazorPageGenerator Console.WriteLine(); } - private static void GenerateCodeFile(string cshtmlFilePath, string rootNamespace) + private static void GenerateCodeFile(RazorTemplateEngine templateEngine, RazorProjectItem projectItem) { - var basePath = Path.GetDirectoryName(cshtmlFilePath); - var fileName = Path.GetFileName(cshtmlFilePath); - var fileNameNoExtension = Path.GetFileNameWithoutExtension(fileName); - var codeLang = new CSharpRazorCodeLanguage(); - var host = new RazorEngineHost(codeLang); - host.DefaultBaseClass = "Microsoft.Extensions.RazorViews.BaseView"; - host.GeneratedClassContext = new GeneratedClassContext( - executeMethodName: GeneratedClassContext.DefaultExecuteMethodName, - writeMethodName: GeneratedClassContext.DefaultWriteMethodName, - writeLiteralMethodName: GeneratedClassContext.DefaultWriteLiteralMethodName, - writeToMethodName: "WriteTo", - writeLiteralToMethodName: "WriteLiteralTo", - templateTypeName: "HelperResult", - defineSectionMethodName: "DefineSection", - generatedTagHelperContext: new GeneratedTagHelperContext()); - var engine = new RazorTemplateEngine(host); + var cSharpDocument = templateEngine.GenerateCode(projectItem); + if (cSharpDocument.Diagnostics.Any()) + { + var diagnostics = string.Join(Environment.NewLine, cSharpDocument.Diagnostics); + Console.WriteLine($"One or more parse errors encountered. This will not prevent the generator from continuing: {Environment.NewLine}{diagnostics}."); + } - var cshtmlContent = File.ReadAllText(cshtmlFilePath); - cshtmlContent = ProcessFileIncludes(basePath, cshtmlContent); - - var generatorResults = engine.GenerateCode( - input: new StringReader(cshtmlContent), - className: fileNameNoExtension, - rootNamespace: Path.GetFileName(rootNamespace), - sourceFileName: fileName); - - var generatedCode = generatorResults.GeneratedCode; - - // Make the generated class 'internal' instead of 'public' - generatedCode = generatedCode.Replace("public class", "internal class"); - - File.WriteAllText(Path.Combine(basePath, string.Format("{0}.Designer.cs", fileNameNoExtension)), generatedCode); + var generatedCodeFilePath = Path.ChangeExtension( + ((FileSystemRazorProjectItem)projectItem).FileInfo.FullName, + ".Designer.cs"); + File.WriteAllText(generatedCodeFilePath, cSharpDocument.GeneratedCode); } - private static string ProcessFileIncludes(string basePath, string cshtmlContent) + private class FileSystemRazorProject : RazorProject { - var startMatch = "<%$ include: "; - var endMatch = " %>"; - var startIndex = 0; - while (startIndex < cshtmlContent.Length) + private readonly string _basePath; + + public FileSystemRazorProject(string basePath) { - startIndex = cshtmlContent.IndexOf(startMatch, startIndex); - if (startIndex == -1) - { - break; - } - var endIndex = cshtmlContent.IndexOf(endMatch, startIndex); - if (endIndex == -1) - { - throw new InvalidOperationException("Invalid include file format. Usage example: <%$ include: ErrorPage.js %>"); - } - var includeFileName = cshtmlContent.Substring(startIndex + startMatch.Length, endIndex - (startIndex + startMatch.Length)); - Console.WriteLine(" Inlining file {0}", includeFileName); - var includeFileContent = File.ReadAllText(Path.Combine(basePath, includeFileName)); - cshtmlContent = cshtmlContent.Substring(0, startIndex) + includeFileContent + cshtmlContent.Substring(endIndex + endMatch.Length); - startIndex = startIndex + includeFileContent.Length; + _basePath = basePath; + } + + public override IEnumerable EnumerateItems(string basePath) + { + return new DirectoryInfo(_basePath) + .EnumerateFiles("*.cshtml", SearchOption.TopDirectoryOnly) + .Select(file => GetItem(basePath, file)); + } + + public override RazorProjectItem GetItem(string path) => throw new NotSupportedException(); + + private RazorProjectItem GetItem(string basePath, FileInfo file) + { + if (!file.Exists) + { + throw new FileNotFoundException($"{file.FullName} does not exist."); + } + + return new FileSystemRazorProjectItem(basePath, file); + } + } + + private class FileSystemRazorProjectItem : RazorProjectItem + { + public FileSystemRazorProjectItem(string basePath, FileInfo fileInfo) + { + BasePath = basePath; + Path = fileInfo.Name; + FileInfo = fileInfo; + } + + public FileInfo FileInfo { get; } + + public override string BasePath { get; } + + public override string Path { get; } + + // Mask the full name since we don't want a developer's local file paths to be commited. + public override string PhysicalPath => FileInfo.Name; + + public override bool Exists => true; + + public override Stream Read() + { + var processedContent = ProcessFileIncludes(); + return new MemoryStream(Encoding.UTF8.GetBytes(processedContent)); + } + + private string ProcessFileIncludes() + { + var basePath = FileInfo.DirectoryName; + var cshtmlContent = File.ReadAllText(FileInfo.FullName); + + var startMatch = "<%$ include: "; + var endMatch = " %>"; + var startIndex = 0; + while (startIndex < cshtmlContent.Length) + { + startIndex = cshtmlContent.IndexOf(startMatch, startIndex); + if (startIndex == -1) + { + break; + } + var endIndex = cshtmlContent.IndexOf(endMatch, startIndex); + if (endIndex == -1) + { + throw new InvalidOperationException($"Invalid include file format in {FileInfo.FullName}. Usage example: <%$ include: ErrorPage.js %>"); + } + var includeFileName = cshtmlContent.Substring(startIndex + startMatch.Length, endIndex - (startIndex + startMatch.Length)); + Console.WriteLine(" Inlining file {0}", includeFileName); + var includeFileContent = File.ReadAllText(System.IO.Path.Combine(basePath, includeFileName)); + cshtmlContent = cshtmlContent.Substring(0, startIndex) + includeFileContent + cshtmlContent.Substring(endIndex + endMatch.Length); + startIndex = startIndex + includeFileContent.Length; + } + return cshtmlContent; } - return cshtmlContent; } } } diff --git a/src/RazorPageGenerator/RazorPageGenerator.csproj b/src/RazorPageGenerator/RazorPageGenerator.csproj index 11dd8e30d4..079a940dbc 100644 --- a/src/RazorPageGenerator/RazorPageGenerator.csproj +++ b/src/RazorPageGenerator/RazorPageGenerator.csproj @@ -1,4 +1,4 @@ - + @@ -11,7 +11,7 @@ - + diff --git a/src/RazorPageGenerator/RemovePragamaChecksumFeature.cs b/src/RazorPageGenerator/RemovePragamaChecksumFeature.cs new file mode 100644 index 0000000000..6ed229693b --- /dev/null +++ b/src/RazorPageGenerator/RemovePragamaChecksumFeature.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace RazorPageGenerator +{ + class RemovePragamaChecksumFeature : RazorIRPassBase, IRazorIROptimizationPass + { + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + var walker = new Walker(); + walker.Visit(irDocument); + + walker.ChecksumNode.Parent.Children.Remove(walker.ChecksumNode); + } + + private class Walker : RazorIRNodeWalker + { + public ChecksumIRNode ChecksumNode { get; private set; } + + public override void VisitChecksum(ChecksumIRNode node) + { + ChecksumNode = node; + } + } + } +} \ No newline at end of file