diff --git a/.gitignore b/.gitignore index 15217d4a09..a0265568eb 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ global.json BenchmarkDotNet.Artifacts/ Microsoft.VisualStudio.RazorExtension.nuget.props Microsoft.VisualStudio.RazorExtension.nuget.targets +msbuild.binlog +msbuild.log diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets index 8ae440eb70..09ffa3e51a 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets @@ -25,9 +25,9 @@ --> + Condition="'@(RazorGenerateWithTargetPath)' != ''"> - + @@ -51,7 +51,7 @@ DependsOnTargets="Compile" Inputs="$(MSBuildAllProjects);@(RazorReferencePath)" Outputs="$(_RazorTagHelperInputCache)" - Condition="'@(RazorGenerate)'!=''"> + Condition="'@(RazorGenerateWithTargetPath)' != ''"> - - <_RazorCoreCompileResourceInputs - Include="@(RazorEmbeddedResource)" - Condition="'%(RazorEmbeddedResource.WithCulture)'=='false' and '%(RazorEmbeddedResource.Type)'=='Non-Resx' " /> diff --git a/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProject.cs b/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProject.cs index d6e623c4f1..d6bfa5596a 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProject.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProject.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Microsoft.AspNetCore.Razor.Language { - internal class FileSystemRazorProject : RazorProject + internal class FileSystemRazorProject : RazorProjectFileSystem { public FileSystemRazorProject(string root) { diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs index c97fe625b4..02e2547355 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs @@ -8,6 +8,8 @@ using System.Runtime.CompilerServices; [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.AspNetCore.Razor.Tools.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("rzc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index 13c56c698e..a785a39bca 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -1870,6 +1870,34 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatRazorLanguageVersion_InvalidVersion(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("RazorLanguageVersion_InvalidVersion"), p0); + /// + /// File path '{0}' does not belong to the directory '{1}'. + /// + internal static string VirtualFileSystem_FileDoesNotBelongToDirectory + { + get => GetString("VirtualFileSystem_FileDoesNotBelongToDirectory"); + } + + /// + /// File path '{0}' does not belong to the directory '{1}'. + /// + internal static string FormatVirtualFileSystem_FileDoesNotBelongToDirectory(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("VirtualFileSystem_FileDoesNotBelongToDirectory"), p0, p1); + + /// + /// The file path '{0}' is invalid. File path is the root relative path of the file starting with '/' and should not contain any '\' characters. + /// + internal static string VirtualFileSystem_InvalidRelativePath + { + get => GetString("VirtualFileSystem_InvalidRelativePath"); + } + + /// + /// The file path '{0}' is invalid. File path is the root relative path of the file starting with '/' and should not contain any '\' characters. + /// + internal static string FormatVirtualFileSystem_InvalidRelativePath(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("VirtualFileSystem_InvalidRelativePath"), p0); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs index 83b86dc8b7..975b821ade 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs @@ -5,5 +5,14 @@ namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorProjectFileSystem : RazorProject { + /// + /// Create a Razor project based on a physical file system. + /// + /// The directory to root the file system at. + /// A + public static new RazorProjectFileSystem Create(string rootDirectoryPath) + { + return new FileSystemRazorProject(rootDirectoryPath); + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index 6413f1038b..195a9d791b 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -536,4 +536,10 @@ Instead, wrap the contents of the block in "{{}}": The Razor language version '{0}' is unrecognized or not supported by this version of Razor. + + File path '{0}' does not belong to the directory '{1}'. + + + The file path '{0}' is invalid. File path is the root relative path of the file starting with '/' and should not contain any '\' characters. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/VirtualRazorProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Language/VirtualRazorProjectFileSystem.cs new file mode 100644 index 0000000000..bb9a75ee3a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/VirtualRazorProjectFileSystem.cs @@ -0,0 +1,216 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class VirtualRazorProjectFileSystem : RazorProjectFileSystem + { + private readonly DirectoryNode Root = new DirectoryNode("/"); + + public override IEnumerable EnumerateItems(string basePath) + { + + basePath = NormalizeAndEnsureValidPath(basePath); + var directory = Root.GetDirectory(basePath); + return directory?.EnumerateItems() ?? Enumerable.Empty(); + } + + public override RazorProjectItem GetItem(string path) + { + path = NormalizeAndEnsureValidPath(path); + return Root.GetItem(path) ?? new NotFoundProjectItem(string.Empty, path); + } + + public void Add(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var filePath = NormalizeAndEnsureValidPath(projectItem.FilePath); + Root.AddFile(new FileNode(filePath, projectItem)); + } + + // Internal for testing + [DebuggerDisplay("{Path}")] + internal class DirectoryNode + { + public DirectoryNode(string path) + { + Path = path; + } + + public string Path { get; } + + public List Directories { get; } = new List(); + + public List Files { get; } = new List(); + + public void AddFile(FileNode fileNode) + { + var filePath = fileNode.Path; + if (!filePath.StartsWith(Path, StringComparison.OrdinalIgnoreCase)) + { + var message = Resources.FormatVirtualFileSystem_FileDoesNotBelongToDirectory(fileNode.Path, Path); + throw new InvalidOperationException(message); + } + + // Look for the first / that appears in the path after the current directory path. + var directoryPath = GetDirectoryPath(filePath); + var directory = GetOrAddDirectory(this, directoryPath, createIfNotExists: true); + Debug.Assert(directory != null); + directory.Files.Add(fileNode); + } + + public DirectoryNode GetDirectory(string path) + { + if (!path.StartsWith(Path, StringComparison.OrdinalIgnoreCase)) + { + var message = Resources.FormatVirtualFileSystem_FileDoesNotBelongToDirectory(path, Path); + throw new InvalidOperationException(message); + } + + return GetOrAddDirectory(this, path); + } + + public IEnumerable EnumerateItems() + { + foreach (var file in Files) + { + yield return file.ProjectItem; + } + + foreach (var directory in Directories) + { + foreach (var file in directory.EnumerateItems()) + { + yield return file; + } + } + } + + public RazorProjectItem GetItem(string path) + { + if (!path.StartsWith(Path, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(Resources.FormatVirtualFileSystem_FileDoesNotBelongToDirectory(path, Path)); + } + + var directoryPath = GetDirectoryPath(path); + var directory = GetOrAddDirectory(this, directoryPath); + if (directory == null) + { + return null; + } + + foreach (var file in directory.Files) + { + var filePath = file.Path; + var directoryLength = directory.Path.Length; + + // path, filePath -> /Views/Home/Index.cshtml + // directory.Path -> /Views/Home/ + // We only need to match the file name portion since we've already matched the directory segment. + if (string.Compare(path, directoryLength, filePath, directoryLength, path.Length - directoryLength, StringComparison.OrdinalIgnoreCase) == 0) + { + return file.ProjectItem; + } + } + + return null; + } + + private static string GetDirectoryPath(string path) + { + // /dir1/dir2/file.cshtml -> /dir1/dir2/ + var fileNameIndex = path.LastIndexOf('/'); + if (fileNameIndex == -1) + { + return path; + } + + return path.Substring(0, fileNameIndex + 1); + } + + private static DirectoryNode GetOrAddDirectory( + DirectoryNode directory, + string path, + bool createIfNotExists = false) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + if (path[path.Length - 1] != '/') + { + path += '/'; + } + + int index; + while ((index = path.IndexOf('/', directory.Path.Length)) != -1 && index != path.Length) + { + var subDirectory = FindSubDirectory(directory, path); + + if (subDirectory == null) + { + if (createIfNotExists) + { + var directoryPath = path.Substring(0, index + 1); // + 1 to include trailing slash + subDirectory = new DirectoryNode(directoryPath); + directory.Directories.Add(subDirectory); + } + else + { + return null; + } + } + + directory = subDirectory; + } + + return directory; + } + + private static DirectoryNode FindSubDirectory(DirectoryNode parentDirectory, string path) + { + for (var i = 0; i < parentDirectory.Directories.Count; i++) + { + // ParentDirectory.Path -> /Views/Home/ + // CurrentDirectory.Path -> /Views/Home/SubDir/ + // Path -> /Views/Home/SubDir/MorePath/File.cshtml + // Each invocation of FindSubDirectory returns the immediate subdirectory along the path to the file. + + var currentDirectory = parentDirectory.Directories[i]; + var directoryPath = currentDirectory.Path; + var startIndex = parentDirectory.Path.Length; + var directoryNameLength = directoryPath.Length - startIndex; + + if (string.Compare(path, startIndex, directoryPath, startIndex, directoryPath.Length - startIndex, StringComparison.OrdinalIgnoreCase) == 0) + { + return currentDirectory; + } + } + + return null; + } + } + + // Internal for testing + [DebuggerDisplay("{Path}")] + internal struct FileNode + { + public FileNode(string path, RazorProjectItem projectItem) + { + Path = path; + ProjectItem = projectItem; + } + + public string Path { get; } + + public RazorProjectItem ProjectItem { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs b/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs index 3b92bed574..c1df3cd431 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs @@ -69,9 +69,9 @@ namespace Microsoft.AspNetCore.Razor.Tasks { if (Debug) { + Log.LogMessage(MessageImportance.High, "Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); while (!Debugger.IsAttached) { - Log.LogMessage(MessageImportance.High, "Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); Thread.Sleep(TimeSpan.FromSeconds(3)); } } @@ -174,5 +174,18 @@ namespace Microsoft.AspNetCore.Razor.Tasks CommandLineUtilities.SplitCommandLineIntoArguments(responseFileCommands, removeHashComments: true); return responseFileArguments.ToList(); } + + protected override bool HandleTaskExecutionErrors() + { + if (!HasLoggedErrors) + { + var toolCommand = Path.GetFileNameWithoutExtension(ToolAssembly) + " " + Command; + // Show a slightly better error than the standard ToolTask message that says "dotnet" failed. + Log.LogError($"{toolCommand} exited with code {ExitCode}."); + return false; + } + + return base.HandleTaskExecutionErrors(); + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs index 06ed7088e2..7389725f94 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs @@ -1,28 +1,44 @@ // 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; -using Microsoft.CodeAnalysis.CommandLine; namespace Microsoft.AspNetCore.Razor.Tasks { public class RazorGenerate : DotNetToolTask { + private const string GeneratedOutput = "GeneratedOutput"; + private const string TargetPath = "TargetPath"; + private const string FullPath = "FullPath"; + [Required] - public string[] Sources { get; set; } + public ITaskItem[] Sources { get; set; } [Required] public string ProjectRoot { get; set; } - [Required] - public string OutputPath { get; set; } - [Required] public string TagHelperManifest { get; set; } internal override string Command => "generate"; + protected override bool ValidateParameters() + { + for (var i = 0; i < Sources.Length; i++) + { + if (!EnsureRequiredMetadata(Sources[i], FullPath) || + !EnsureRequiredMetadata(Sources[i], GeneratedOutput) || + !EnsureRequiredMetadata(Sources[i], TargetPath)) + { + return false; + } + } + + return base.ValidateParameters(); + } + protected override string GenerateResponseFileCommands() { var builder = new StringBuilder(); @@ -31,19 +47,37 @@ namespace Microsoft.AspNetCore.Razor.Tasks for (var i = 0; i < Sources.Length; i++) { - builder.AppendLine(Sources[i]); + var input = Sources[i]; + builder.AppendLine("-s"); + builder.AppendLine(input.GetMetadata(FullPath)); + + builder.AppendLine("-r"); + builder.AppendLine(input.GetMetadata(TargetPath)); + + builder.AppendLine("-o"); + var outputPath = Path.Combine(ProjectRoot, input.GetMetadata(GeneratedOutput)); + builder.AppendLine(outputPath); } builder.AppendLine("-p"); builder.AppendLine(ProjectRoot); - builder.AppendLine("-o"); - builder.AppendLine(OutputPath); - builder.AppendLine("-t"); builder.AppendLine(TagHelperManifest); return builder.ToString(); } + + private bool EnsureRequiredMetadata(ITaskItem item, string metadataName) + { + var value = item.GetMetadata(metadataName); + if (string.IsNullOrEmpty(value)) + { + Log.LogError($"Missing required metadata '{metadataName}' for '{item.ItemSpec}."); + return false; + } + + return true; + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs new file mode 100644 index 0000000000..5102cf056b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class CompositeRazorProjectFileSystem : RazorProjectFileSystem + { + public CompositeRazorProjectFileSystem(IReadOnlyList projects) + { + Projects = projects ?? throw new ArgumentNullException(nameof(projects)); + } + + public IReadOnlyList Projects { get; } + + public override IEnumerable EnumerateItems(string basePath) + { + foreach (var project in Projects) + { + foreach (var result in project.EnumerateItems(basePath)) + { + yield return result; + } + } + } + + public override RazorProjectItem GetItem(string path) + { + RazorProjectItem razorProjectItem = null; + foreach (var project in Projects) + { + razorProjectItem = project.GetItem(path); + if (razorProjectItem != null && razorProjectItem.Exists) + { + return razorProjectItem; + } + } + + return razorProjectItem; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DebugMode.cs b/src/Microsoft.AspNetCore.Razor.Tools/DebugMode.cs index 7d9815cd5f..5d38654c52 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DebugMode.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DebugMode.cs @@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Razor.Tools { args = args.Skip(1).ToArray(); + Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); while (!Debugger.IsAttached) { - Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); Thread.Sleep(TimeSpan.FromSeconds(3)); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs index 41c935b8b1..9fdedccd93 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs @@ -13,20 +13,30 @@ using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Tools { + internal class Builder + { + public static Builder Make(CommandBase result) => null; + + public static Builder Make(T result) => null; + } + internal class GenerateCommand : CommandBase { public GenerateCommand(Application parent) : base(parent, "generate") { - Sources = Argument("sources", ".cshtml files to compile", multipleValues: true); + Sources = Option("-s", ".cshtml files to compile", CommandOptionType.MultipleValue); + Outputs = Option("-o", "Generated output file path", CommandOptionType.MultipleValue); + RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); - OutputDirectory = Option("-o", "output directory", CommandOptionType.SingleValue); TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); } - public CommandArgument Sources { get; } + public CommandOption Sources { get; } - public CommandOption OutputDirectory { get; } + public CommandOption Outputs { get; } + + public CommandOption RelativePaths { get; } public CommandOption ProjectDirectory { get; } @@ -36,25 +46,30 @@ namespace Microsoft.AspNetCore.Razor.Tools { var result = ExecuteCore( projectDirectory: ProjectDirectory.Value(), - outputDirectory: OutputDirectory.Value(), tagHelperManifest: TagHelperManifest.Value(), - sources: Sources.Values.ToArray()); + sources: Sources.Values, + outputs: Outputs.Values, + relativePaths: RelativePaths.Values); return Task.FromResult(result); } protected override bool ValidateArguments() { - if (string.IsNullOrEmpty(OutputDirectory.Value())) + if (Sources.Values.Count == 0) { - Error.WriteLine($"{OutputDirectory.ValueName} not specified."); + Error.WriteLine($"{Sources.ValueName} should have at least one value."); return false; } - if (Sources.Values.Count == 0) + if (Outputs.Values.Count != Sources.Values.Count) { - Error.WriteLine($"{Sources.Name} should have at least one value."); - return false; + Error.WriteLine($"{Sources.ValueName} has {Sources.Values.Count}, but {Outputs.ValueName} has {Outputs.Values.Count}."); + } + + if (RelativePaths.Values.Count != Sources.Values.Count) + { + Error.WriteLine($"{Sources.ValueName} has {Sources.Values.Count}, but {RelativePaths.ValueName} has {RelativePaths.Values.Count}."); } if (string.IsNullOrEmpty(ProjectDirectory.Value())) @@ -65,10 +80,14 @@ namespace Microsoft.AspNetCore.Razor.Tools return true; } - private int ExecuteCore(string projectDirectory, string outputDirectory, string tagHelperManifest, string[] sources) + private int ExecuteCore( + string projectDirectory, + string tagHelperManifest, + List sources, + List outputs, + List relativePaths) { tagHelperManifest = Path.Combine(projectDirectory, tagHelperManifest); - outputDirectory = Path.Combine(projectDirectory, outputDirectory); var tagHelpers = GetTagHelpers(tagHelperManifest); @@ -79,10 +98,18 @@ namespace Microsoft.AspNetCore.Razor.Tools 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 inputItems = GetInputItems(projectDirectory, sources, outputs, relativePaths); + var compositeProject = new CompositeRazorProjectFileSystem( + new[] + { + GetVirtualRazorProjectSystem(inputItems), + RazorProjectFileSystem.Create(projectDirectory), + }); + + var templateEngine = new MvcRazorTemplateEngine(engine, compositeProject); + + var results = GenerateCode(templateEngine, inputItems); var success = true; @@ -97,16 +124,30 @@ namespace Microsoft.AspNetCore.Razor.Tools } } - var viewFile = result.ViewFileInfo.ViewEnginePath.Substring(1); - var outputFileName = Path.ChangeExtension(viewFile, ".cs"); - - var outputFilePath = Path.Combine(outputDirectory, outputFileName); + var outputFilePath = result.InputItem.OutputPath; File.WriteAllText(outputFilePath, result.CSharpDocument.GeneratedCode); } return success ? 0 : -1; } + private VirtualRazorProjectFileSystem GetVirtualRazorProjectSystem(SourceItem[] inputItems) + { + var project = new VirtualRazorProjectFileSystem(); + foreach (var item in inputItems) + { + var projectItem = new FileSystemRazorProjectItem( + basePath: "/", + filePath: item.FilePath, + relativePhysicalPath: item.RelativePhysicalPath, + file: new FileInfo(item.SourcePath)); + + project.Add(projectItem); + } + + return project; + } + private IReadOnlyList GetTagHelpers(string tagHelperManifest) { if (!File.Exists(tagHelperManifest)) @@ -127,33 +168,27 @@ namespace Microsoft.AspNetCore.Razor.Tools } } - private List GetRazorFiles(string projectDirectory, string[] sources) + private SourceItem[] GetInputItems(string projectDirectory, List sources, List outputs, List relativePath) { - var trimLength = projectDirectory.EndsWith("/") ? projectDirectory.Length - 1 : projectDirectory.Length; - - var items = new List(sources.Length); - for (var i = 0; i < sources.Length; i++) + var items = new SourceItem[sources.Count]; + for (var i = 0; i < items.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)); - } + var outputPath = Path.Combine(projectDirectory, outputs[i]); + items[i] = new SourceItem(sources[i], outputs[i], relativePath[i]); } return items; } - private OutputItem[] GenerateCode(RazorTemplateEngine templateEngine, IReadOnlyList sources) + private OutputItem[] GenerateCode(RazorTemplateEngine templateEngine, SourceItem[] inputs) { - var outputs = new OutputItem[sources.Count]; + var outputs = new OutputItem[inputs.Length]; Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = 4 }, i => { - var source = sources[i]; + var inputItem = inputs[i]; - var csharpDocument = templateEngine.GenerateCode(source.ViewEnginePath); - outputs[i] = new OutputItem(source, csharpDocument); + var csharpDocument = templateEngine.GenerateCode(inputItem.FilePath); + outputs[i] = new OutputItem(inputItem, csharpDocument); }); return outputs; @@ -162,43 +197,37 @@ namespace Microsoft.AspNetCore.Razor.Tools private struct OutputItem { public OutputItem( - SourceItem viewFileInfo, + SourceItem inputItem, RazorCSharpDocument cSharpDocument) { - ViewFileInfo = viewFileInfo; + InputItem = inputItem; CSharpDocument = cSharpDocument; } - public SourceItem ViewFileInfo { get; } + public SourceItem InputItem { get; } public RazorCSharpDocument CSharpDocument { get; } } private struct SourceItem { - public SourceItem(string fullPath, string viewEnginePath) + public SourceItem(string sourcePath, string outputPath, string physicalRelativePath) { - FullPath = fullPath; - ViewEnginePath = viewEnginePath; + SourcePath = sourcePath; + OutputPath = outputPath; + RelativePhysicalPath = physicalRelativePath; + FilePath = '/' + physicalRelativePath + .Replace(Path.DirectorySeparatorChar, '/') + .Replace("//", "/"); } - public string FullPath { get; } + public string SourcePath { get; } - public string ViewEnginePath { get; } + public string OutputPath { 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); - } + public string RelativePhysicalPath { get; } + + public string FilePath { get; } } private class StaticTagHelperFeature : ITagHelperFeature @@ -210,4 +239,4 @@ namespace Microsoft.AspNetCore.Razor.Tools public IReadOnlyList GetDescriptors() => TagHelpers; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets index 99109a78d4..fe80f16785 100644 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets @@ -31,6 +31,7 @@ Copyright (c) .NET Foundation. All rights reserved. ResolveRazorGenerateInputs; + AssignRazorGenerateTargetPaths; ResolveAssemblyReferenceRazorGenerateInputs; _EnsureRazorCompilerReferenced; ResolveTagHelperRazorGenerateInputs @@ -48,6 +49,7 @@ Copyright (c) .NET Foundation. All rights reserved. + ResolveRazorEmbeddedResources @@ -221,11 +223,17 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + - - $(RazorGenerateIntermediateOutputPath)%(RelativeDir)%(Filename).cs - + + $(RazorGenerateIntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(RazorGenerateWithTargetPath.TargetPath)', '.cs')) + @@ -249,6 +257,21 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + /$([System.String]::Copy('%(RazorGenerateWithTargetPath.TargetPath)').Replace('\','/')) + Non-Resx + false + + + + <_RazorCoreCompileResourceInputs + Include="@(RazorEmbeddedResource)" + Condition="'%(RazorEmbeddedResource.WithCulture)'=='false' and '%(RazorEmbeddedResource.Type)'=='Non-Resx' " /> + + +