Add support for relative paths

* Move path munging in to Razor SDK
* Use AssignTargetPath to determine the target path for outputs and embedded resources

Fixes #1829
Fixes #1847
Fixes #1999
This commit is contained in:
Pranav K 2018-01-29 21:04:27 -08:00
parent 5a93f68ccc
commit 84beb5985f
29 changed files with 1088 additions and 126 deletions

2
.gitignore vendored
View File

@ -36,3 +36,5 @@ global.json
BenchmarkDotNet.Artifacts/
Microsoft.VisualStudio.RazorExtension.nuget.props
Microsoft.VisualStudio.RazorExtension.nuget.targets
msbuild.binlog
msbuild.log

View File

@ -25,9 +25,9 @@
-->
<Target
Name="_HashRazorGenerateInputs"
Condition="'@(RazorGenerate)'!=''">
Condition="'@(RazorGenerateWithTargetPath)' != ''">
<Hash ItemsToHash="@(RazorGenerate)">
<Hash ItemsToHash="@(RazorGenerateWithTargetPath)">
<Output TaskParameter="HashResult" PropertyName="_RazorGenerateInputsHash" />
</Hash>
@ -51,7 +51,7 @@
DependsOnTargets="Compile"
Inputs="$(MSBuildAllProjects);@(RazorReferencePath)"
Outputs="$(_RazorTagHelperInputCache)"
Condition="'@(RazorGenerate)'!=''">
Condition="'@(RazorGenerateWithTargetPath)' != ''">
<!--
We're manipulating our output directly here because we want to separate the actual up-to-date check
@ -81,9 +81,13 @@
</RazorTagHelper>
</Target>
<Target Name="_ResolveRazorGenerateOutputs">
<Target Name="_ResolveRazorGenerateOutputs" Condition="'@(RazorGenerateWithTargetPath)' != ''">
<Error
Text="RazorGenerateWithTargetPath item '%(RazorGenerateWithTargetPath.Identity)' does not specify required metadata 'GeneratedOutput'."
Condition="'%(RazorGenerateWithTargetPath.GeneratedOutput)' == ''" />
<ItemGroup>
<_RazorGenerateOutput Include="%(RazorGenerate.GeneratedOutput)" Condition="'%(RazorGenerate.GeneratedOutput)'!=''"/>
<_RazorGenerateOutput Include="%(RazorGenerateWithTargetPath.GeneratedOutput)" />
</ItemGroup>
</Target>
@ -97,9 +101,9 @@
<Target
Name="RazorCoreGenerate"
DependsOnTargets="$(RazorCoreGenerateDependsOn)"
Inputs="$(MSBuildAllProjects);$(_RazorGenerateInputsHashFile);$(_RazorTagHelperOutputCache);@(RazorGenerate)"
Inputs="$(MSBuildAllProjects);$(_RazorGenerateInputsHashFile);$(_RazorTagHelperOutputCache);@(RazorGenerateWithTargetPath)"
Outputs="@(_RazorGenerateOutput)"
Condition="'@(RazorGenerate)'!= ''">
Condition="'@(RazorGenerateWithTargetPath)'!= ''">
<RemoveDir
Directories="$(RazorGenerateIntermediateOutputPath)"
@ -115,10 +119,9 @@
ToolAssembly="$(_RazorToolAssembly)"
UseServer="$(UseRazorBuildServer)"
PipeName="$(_RazorBuildServerPipeName)"
Sources="@(RazorGenerate)"
Sources="@(RazorGenerateWithTargetPath)"
ProjectRoot="$(MSBuildProjectDirectory)"
TagHelperManifest="$(_RazorTagHelperOutputCache)"
OutputPath="$(RazorGenerateIntermediateOutputPath)" />
TagHelperManifest="$(_RazorTagHelperOutputCache)" />
<ItemGroup>
<FileWrites Include="@(_RazorGenerateOutput)" />
@ -132,18 +135,6 @@
<Target Name="_ResolveGeneratedRazorCompileInputs">
<ItemGroup>
<RazorCompile Include="@(_RazorGenerateOutput)" />
<RazorEmbeddedResource Include="@(RazorGenerate)" Condition="$(EmbedRazorGenerateSources)">
<LogicalName>/$([System.String]::Copy('%(Identity)').Replace('\','/'))</LogicalName>
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
</RazorEmbeddedResource>
</ItemGroup>
<!-- Similar to _GenerateCompileInputs -->
<ItemGroup>
<_RazorCoreCompileResourceInputs
Include="@(RazorEmbeddedResource)"
Condition="'%(RazorEmbeddedResource.WithCulture)'=='false' and '%(RazorEmbeddedResource.Type)'=='Non-Resx' " />
</ItemGroup>
</Target>

View File

@ -8,7 +8,7 @@ using System.Linq;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class FileSystemRazorProject : RazorProject
internal class FileSystemRazorProject : RazorProjectFileSystem
{
public FileSystemRazorProject(string root)
{

View File

@ -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")]

View File

@ -1870,6 +1870,34 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatRazorLanguageVersion_InvalidVersion(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("RazorLanguageVersion_InvalidVersion"), p0);
/// <summary>
/// File path '{0}' does not belong to the directory '{1}'.
/// </summary>
internal static string VirtualFileSystem_FileDoesNotBelongToDirectory
{
get => GetString("VirtualFileSystem_FileDoesNotBelongToDirectory");
}
/// <summary>
/// File path '{0}' does not belong to the directory '{1}'.
/// </summary>
internal static string FormatVirtualFileSystem_FileDoesNotBelongToDirectory(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("VirtualFileSystem_FileDoesNotBelongToDirectory"), p0, p1);
/// <summary>
/// The file path '{0}' is invalid. File path is the root relative path of the file starting with '/' and should not contain any '\' characters.
/// </summary>
internal static string VirtualFileSystem_InvalidRelativePath
{
get => GetString("VirtualFileSystem_InvalidRelativePath");
}
/// <summary>
/// The file path '{0}' is invalid. File path is the root relative path of the file starting with '/' and should not contain any '\' characters.
/// </summary>
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);

View File

@ -5,5 +5,14 @@ namespace Microsoft.AspNetCore.Razor.Language
{
public abstract class RazorProjectFileSystem : RazorProject
{
/// <summary>
/// Create a Razor project based on a physical file system.
/// </summary>
/// <param name="rootDirectoryPath">The directory to root the file system at.</param>
/// <returns>A <see cref="RazorProject"/></returns>
public static new RazorProjectFileSystem Create(string rootDirectoryPath)
{
return new FileSystemRazorProject(rootDirectoryPath);
}
}
}

View File

@ -536,4 +536,10 @@ Instead, wrap the contents of the block in "{{}}":
<data name="RazorLanguageVersion_InvalidVersion" xml:space="preserve">
<value>The Razor language version '{0}' is unrecognized or not supported by this version of Razor.</value>
</data>
<data name="VirtualFileSystem_FileDoesNotBelongToDirectory" xml:space="preserve">
<value>File path '{0}' does not belong to the directory '{1}'.</value>
</data>
<data name="VirtualFileSystem_InvalidRelativePath" xml:space="preserve">
<value>The file path '{0}' is invalid. File path is the root relative path of the file starting with '/' and should not contain any '\' characters.</value>
</data>
</root>

View File

@ -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<RazorProjectItem> EnumerateItems(string basePath)
{
basePath = NormalizeAndEnsureValidPath(basePath);
var directory = Root.GetDirectory(basePath);
return directory?.EnumerateItems() ?? Enumerable.Empty<RazorProjectItem>();
}
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<DirectoryNode> Directories { get; } = new List<DirectoryNode>();
public List<FileNode> Files { get; } = new List<FileNode>();
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<RazorProjectItem> 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; }
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -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<RazorProjectFileSystem> projects)
{
Projects = projects ?? throw new ArgumentNullException(nameof(projects));
}
public IReadOnlyList<RazorProjectFileSystem> Projects { get; }
public override IEnumerable<RazorProjectItem> 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;
}
}
}

View File

@ -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));
}
}

View File

@ -13,20 +13,30 @@ using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Razor.Tools
{
internal class Builder<T>
{
public static Builder<T> Make(CommandBase result) => null;
public static Builder<T> 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<string> sources,
List<string> outputs,
List<string> 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<TagHelperDescriptor> GetTagHelpers(string tagHelperManifest)
{
if (!File.Exists(tagHelperManifest))
@ -127,33 +168,27 @@ namespace Microsoft.AspNetCore.Razor.Tools
}
}
private List<SourceItem> GetRazorFiles(string projectDirectory, string[] sources)
private SourceItem[] GetInputItems(string projectDirectory, List<string> sources, List<string> outputs, List<string> relativePath)
{
var trimLength = projectDirectory.EndsWith("/") ? projectDirectory.Length - 1 : projectDirectory.Length;
var items = new List<SourceItem>(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<SourceItem> 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<TagHelperDescriptor> GetDescriptors() => TagHelpers;
}
}
}
}

View File

@ -31,6 +31,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<PropertyGroup>
<PrepareForRazorGenerateDependsOn>
ResolveRazorGenerateInputs;
AssignRazorGenerateTargetPaths;
ResolveAssemblyReferenceRazorGenerateInputs;
_EnsureRazorCompilerReferenced;
ResolveTagHelperRazorGenerateInputs
@ -48,6 +49,7 @@ Copyright (c) .NET Foundation. All rights reserved.
</PrepareForRazorCompileDependsOn>
<ResolveRazorCompileInputsDependsOn>
ResolveRazorEmbeddedResources
</ResolveRazorCompileInputsDependsOn>
<RazorCompileDependsOn>
@ -221,11 +223,17 @@ Copyright (c) .NET Foundation. All rights reserved.
<ItemGroup Condition="'$(EnableDefaultRazorGenerateItems)'=='true'">
<RazorGenerate Include="@(Content)" Condition="'%(Content.Extension)'=='.cshtml'" />
</ItemGroup>
</Target>
<Target Name="AssignRazorGenerateTargetPaths" Condition="'@(RazorGenerate)' != ''">
<AssignTargetPath Files="@(RazorGenerate)" RootFolder="$(MSBuildProjectDirectory)">
<Output TaskParameter="AssignedFiles" ItemName="RazorGenerateWithTargetPath" />
</AssignTargetPath>
<ItemGroup>
<RazorGenerate Update="@(RazorGenerate)" Condtion="'%(RazorGenerate.GeneratedOutput)'==''">
<GeneratedOutput>$(RazorGenerateIntermediateOutputPath)%(RelativeDir)%(Filename).cs</GeneratedOutput>
</RazorGenerate>
<RazorGenerateWithTargetPath Condition="'%(RazorGenerateWithTargetPath.GeneratedOutput)' == ''">
<GeneratedOutput>$(RazorGenerateIntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(RazorGenerateWithTargetPath.TargetPath)', '.cs'))</GeneratedOutput>
</RazorGenerateWithTargetPath>
</ItemGroup>
</Target>
@ -249,6 +257,21 @@ Copyright (c) .NET Foundation. All rights reserved.
<Target Name="ResolveRazorCompileInputs" DependsOnTargets="$(ResolveRazorCompileInputsDependsOn)">
</Target>
<Target Name="ResolveRazorEmbeddedResources" Condition="'$(EmbedRazorGenerateSources)'=='true'">
<ItemGroup>
<RazorEmbeddedResource Include="@(RazorGenerateWithTargetPath)">
<LogicalName>/$([System.String]::Copy('%(RazorGenerateWithTargetPath.TargetPath)').Replace('\','/'))</LogicalName>
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
</RazorEmbeddedResource>
<!-- Similar to _GenerateCompileInputs -->
<_RazorCoreCompileResourceInputs
Include="@(RazorEmbeddedResource)"
Condition="'%(RazorEmbeddedResource.WithCulture)'=='false' and '%(RazorEmbeddedResource.Type)'=='Non-Resx' " />
</ItemGroup>
</Target>
<!--
This target is called after PrepareForPublish when RazorCompileOnPublish=true so that we can hook into publish.
This target just hooks up other targets since Publish and PrepareForPublish don't have a DependsOnTargets

View File

@ -314,7 +314,6 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
}
}
private class BuildFailedException : MSBuildXunitException
{
public BuildFailedException(MSBuildResult result)

View File

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.DotNet.PlatformAbstractions;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll");
Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb");
if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Darwin)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// GetFullPath on OSX doesn't work well in travis. We end up computing a different path than will
// end up in the MSBuild logs.
@ -194,5 +194,27 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
Assert.FileExists(result, OutputPath, "ClassLibrary.PrecompiledViews.dll");
Assert.FileExists(result, OutputPath, "ClassLibrary.PrecompiledViews.pdb");
}
[Fact]
[InitializeTestProject("SimplePages", "LinkedDir")]
public async Task Build_SetsUpEmbeddedResourcesWithLogicalName()
{
// Arrange
var additionalProjectContent = @"
<ItemGroup>
<Content Include=""..\LinkedDir\LinkedFile.cshtml"" Link=""LinkedFileOut\LinkedFile.cshtml"" />
</ItemGroup>
";
AddProjectFileContent(additionalProjectContent);
Directory.CreateDirectory(Path.Combine(Project.DirectoryPath, "..", "LinkedDir"));
var result = await DotnetMSBuild("Build", "/t:_IntrospectRazorEmbeddedResources /p:RazorCompileOnBuild=true /p:EmbedRazorGenerateSources=true");
Assert.BuildPassed(result);
Assert.BuildOutputContainsLine(result, $@"CompileResource: {Path.Combine("Pages", "Index.cshtml")} /Pages/Index.cshtml");
Assert.BuildOutputContainsLine(result, $@"CompileResource: {Path.Combine("Areas", "Products", "Pages", "_ViewStart.cshtml")} /Areas/Products/Pages/_ViewStart.cshtml");
Assert.BuildOutputContainsLine(result, $@"CompileResource: {Path.Combine("..", "LinkedDir", "LinkedFile.cshtml")} /LinkedFileOut/LinkedFile.cshtml");
}
}
}

View File

@ -1,9 +1,7 @@
// 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.Threading.Tasks;
using Microsoft.DotNet.PlatformAbstractions;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests

View File

@ -5,10 +5,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.AspNetCore.Testing;
using Moq;
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
{
@ -100,7 +98,7 @@ $@"<Project>
</Project>";
File.WriteAllText(Path.Combine(projectDestination, "Before.Directory.Build.props"), beforeDirectoryPropsContent);
new List<string> { "Directory.Build.props", "Directory.Build.targets" }
new List<string> { "Directory.Build.props", "Directory.Build.targets", "RazorTest.Introspection.targets" }
.ForEach(file =>
{
var source = Path.Combine(testAppsRoot, file);

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
@ -196,7 +197,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
// We shouldn't need to hash the files
Assert.FileDoesNotExist(result, Path.Combine(IntermediateOutputPath, "SimpleMvc.RazorCoreGenerate.cache"));
Assert.FileCountEquals(result, 0, RazorIntermediateOutputPath, "*.cs");
}
@ -204,14 +205,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
[InitializeTestProject("SimpleMvc")]
public async Task RazorGenerate_MvcRazorFilesToCompile_OverridesDefaultItems()
{
var content = @"
<Project>
<ItemGroup>
<MvcRazorFilesToCompile Include=""Views/Home/About.cshtml"" />
</ItemGroup>
</Project>
var projectContent = @"
<ItemGroup>
<MvcRazorFilesToCompile Include=""Views/Home/About.cshtml"" />
</ItemGroup>
";
File.WriteAllText(Path.Combine(Project.DirectoryPath, "After.Directory.Build.props"), content);
AddProjectFileContent(projectContent);
var result = await DotnetMSBuild(RazorGenerateTarget);
@ -225,17 +224,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
[InitializeTestProject("SimpleMvc")]
public async Task RazorGenerate_EnableDefaultRazorGenerateItems_False_OverridesDefaultItems()
{
var content = @"
<Project>
<PropertyGroup>
<EnableDefaultRazorGenerateItems>false</EnableDefaultRazorGenerateItems>
</PropertyGroup>
<ItemGroup>
<RazorGenerate Include=""Views/Home/About.cshtml"" />
</ItemGroup>
</Project>
var projectContent = @"
<PropertyGroup>
<EnableDefaultRazorGenerateItems>false</EnableDefaultRazorGenerateItems>
</PropertyGroup>
<ItemGroup>
<RazorGenerate Include=""Views/Home/About.cshtml"" />
</ItemGroup>
";
File.WriteAllText(Path.Combine(Project.DirectoryPath, "After.Directory.Build.props"), content);
AddProjectFileContent(projectContent);
var result = await DotnetMSBuild(RazorGenerateTarget);
@ -245,9 +242,61 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
Assert.FileCountEquals(result, 1, RazorIntermediateOutputPath, "*.cs");
}
[Fact(Skip = "Fails due to #1999")]
[Fact]
[InitializeTestProject("SimpleMvc", "LinkedDir")]
public async Task RazorGenerate_WorksWithLinkedFiles()
{
// Arrange
var projectContent = @"
<ItemGroup>
<Content Include=""..\LinkedDir\LinkedFile.cshtml"" />
<Content Include=""..\LinkedDir\LinkedFile2.cshtml"" Link=""LinkedFileOut\LinkedFile2.cshtml"" />
<Content Include=""..\LinkedDir\LinkedFile3.cshtml"" Link=""LinkedFileOut\LinkedFileWithRename.cshtml"" />
</ItemGroup>
";
AddProjectFileContent(projectContent);
var result = await DotnetMSBuild(RazorGenerateTarget, "/t:_IntrospectRazorGenerateWithTargetPath");
Assert.BuildPassed(result);
Assert.FileExists(result, RazorIntermediateOutputPath, "LinkedFile.cs");
Assert.FileExists(result, RazorIntermediateOutputPath, "LinkedFileOut", "LinkedFile2.cs");
Assert.FileExists(result, RazorIntermediateOutputPath, "LinkedFileOut", "LinkedFileWithRename.cs");
Assert.BuildOutputContainsLine(result, $@"RazorGenerateWithTargetPath: {Path.Combine("..", "LinkedDir", "LinkedFile.cshtml")} LinkedFile.cshtml {Path.Combine(RazorIntermediateOutputPath, "LinkedFile.cs")}");
Assert.BuildOutputContainsLine(result, $@"RazorGenerateWithTargetPath: {Path.Combine("..", "LinkedDir", "LinkedFile2.cshtml")} LinkedFileOut\LinkedFile2.cshtml {Path.Combine(RazorIntermediateOutputPath, "LinkedFileOut", "LinkedFile2.cs")}");
Assert.BuildOutputContainsLine(result, $@"RazorGenerateWithTargetPath: {Path.Combine("..", "LinkedDir", "LinkedFile3.cshtml")} LinkedFileOut\LinkedFileWithRename.cshtml {Path.Combine(RazorIntermediateOutputPath, "LinkedFileOut", "LinkedFileWithRename.cs")}");
}
[Fact]
[InitializeTestProject("SimpleMvc", "LinkedDir")]
public async Task RazorGenerate_PrintsErrorsFromLinkedFiles()
{
// Arrange
var file = @"..\LinkedDir\LinkedErrorFile.cshtml";
var projectContent = $@"
<ItemGroup>
<Content Include=""{file}"" Link=""LinkedFileOut\LinkedFile.cshtml"" />
</ItemGroup>
";
AddProjectFileContent(projectContent);
var result = await DotnetMSBuild(RazorGenerateTarget);
Assert.BuildFailed(result);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// GetFullPath on OSX doesn't work well in travis. We end up computing a different path than will
// end up in the MSBuild logs.
var errorLocation = Path.GetFullPath(Path.Combine(Project.DirectoryPath, "..", "LinkedDir", "LinkedErrorFile.cshtml")) + "(1,2)";
Assert.BuildError(result, "RZ1006", errorLocation);
}
}
[Fact]
[InitializeTestProject("SimpleMvc")]
public async Task RazorGenerate_FileWithAbsolutePath_IgnoresFile()
public async Task RazorGenerate_FileWithAbsolutePath()
{
// In preview1 we totally ignore files that are specified with an absolute path
var filePath = Path.Combine(Project.SolutionPath, "temp.cshtml");
@ -258,7 +307,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
<Content Include=""{filePath}""/>
</ItemGroup>");
var result = await DotnetMSBuild(RazorGenerateTarget);
var result = await DotnetMSBuild(RazorGenerateTarget, "/t:_IntrospectRazorGenerateWithTargetPath");
Assert.BuildPassed(result);
@ -274,7 +323,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Shared", "_Layout.cs");
Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Shared", "_ValidationScriptsPartial.cs");
Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Shared", "Error.cs");
Assert.FileCountEquals(result, 8, RazorIntermediateOutputPath, "*.cs");
Assert.FileExists(result, RazorIntermediateOutputPath, "temp.cs");
Assert.FileCountEquals(result, 9, RazorIntermediateOutputPath, "*.cs");
Assert.BuildOutputContainsLine(result, $@"RazorGenerateWithTargetPath: {filePath} temp.cshtml {Path.Combine(RazorIntermediateOutputPath, "temp.cs")}");
}
}
}

View File

@ -0,0 +1,400 @@
// 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 Xunit;
using DirectoryNode = Microsoft.AspNetCore.Razor.Language.VirtualRazorProjectFileSystem.DirectoryNode;
using FileNode = Microsoft.AspNetCore.Razor.Language.VirtualRazorProjectFileSystem.FileNode;
namespace Microsoft.AspNetCore.Razor.Language
{
public class VirtualRazorProjectFileSystemTest
{
[Fact]
public void GetItem_ReturnsNotFound_IfFileDoesNotExistInRoot()
{
// Arrange
var path = "/root-file.cshtml";
var projectSystem = new VirtualRazorProjectFileSystem();
// Act
projectSystem.Add(new TestRazorProjectItem("/different-file.cshtml"));
var result = projectSystem.GetItem(path);
// Assert
Assert.False(result.Exists);
}
[Fact]
public void GetItem_ReturnsItemAddedToRoot()
{
// Arrange
var path = "/root-file.cshtml";
var projectSystem = new VirtualRazorProjectFileSystem();
var projectItem = new TestRazorProjectItem(path);
// Act
projectSystem.Add(projectItem);
var actual = projectSystem.GetItem(path);
// Assert
Assert.Same(projectItem, actual);
}
[Theory]
[InlineData("/subDirectory/file.cshtml")]
[InlineData("/subDirectory/dir2/file.cshtml")]
[InlineData("/subDirectory/dir2/dir3/file.cshtml")]
public void GetItem_ReturnsItemAddedToNestedDirectory(string path)
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
var projectItem = new TestRazorProjectItem(path);
// Act
projectSystem.Add(projectItem);
var actual = projectSystem.GetItem(path);
// Assert
Assert.Same(projectItem, actual);
}
[Fact]
public void GetItem_ReturnsNotFound_WhenNestedDirectoryDoesNotExist()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
// Act
var actual = projectSystem.GetItem("/subDirectory/dir3/file.cshtml");
// Assert
Assert.False(actual.Exists);
}
[Fact]
public void GetItem_ReturnsNotFound_WhenNestedDirectoryDoesNotExist_AndPeerDirectoryExists()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
var projectItem = new TestRazorProjectItem("/subDirectory/dir2/file.cshtml");
// Act
projectSystem.Add(projectItem);
var actual = projectSystem.GetItem("/subDirectory/dir3/file.cshtml");
// Assert
Assert.False(actual.Exists);
}
[Fact]
public void GetItem_ReturnsNotFound_WhenFileDoesNotExistInNestedDirectory()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
var projectItem = new TestRazorProjectItem("/subDirectory/dir2/file.cshtml");
// Act
projectSystem.Add(projectItem);
var actual = projectSystem.GetItem("/subDirectory/dir2/file2.cshtml");
// Assert
Assert.False(actual.Exists);
}
[Fact]
public void EnumerateItems_AtRoot_ReturnsAllFiles()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
var file1 = new TestRazorProjectItem("/subDirectory/dir2/file1.cshtml");
var file2 = new TestRazorProjectItem("/file2.cshtml");
var file3 = new TestRazorProjectItem("/dir3/file3.cshtml");
var file4 = new TestRazorProjectItem("/subDirectory/file4.cshtml");
projectSystem.Add(file1);
projectSystem.Add(file2);
projectSystem.Add(file3);
projectSystem.Add(file4);
// Act
var result = projectSystem.EnumerateItems("/");
// Assert
Assert.Equal(new[] { file2, file4, file1, file3 }, result);
}
[Fact]
public void EnumerateItems_AtSubDirectory_ReturnsAllFilesUnderDirectoryHierarchy()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
var file1 = new TestRazorProjectItem("/subDirectory/dir2/file1.cshtml");
var file2 = new TestRazorProjectItem("/file2.cshtml");
var file3 = new TestRazorProjectItem("/dir3/file3.cshtml");
var file4 = new TestRazorProjectItem("/subDirectory/file4.cshtml");
projectSystem.Add(file1);
projectSystem.Add(file2);
projectSystem.Add(file3);
projectSystem.Add(file4);
// Act
var result = projectSystem.EnumerateItems("/subDirectory");
// Assert
Assert.Equal(new[] { file4, file1 }, result);
}
[Fact]
public void EnumerateItems_WithNoFilesInRoot_ReturnsEmptySequence()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
// Act
var result = projectSystem.EnumerateItems("/");
// Assert
Assert.Empty(result);
}
[Fact]
public void EnumerateItems_ForNonExistentDirectory_ReturnsEmptySequence()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
projectSystem.Add(new TestRazorProjectItem("/subDirectory/dir2/file1.cshtml"));
projectSystem.Add(new TestRazorProjectItem("/file2.cshtml"));
// Act
var result = projectSystem.EnumerateItems("/dir3");
// Assert
Assert.Empty(result);
}
[Fact]
public void GetHierarchicalItems_Works()
{
// Arrange
var projectSystem = new VirtualRazorProjectFileSystem();
var viewImport1 = new TestRazorProjectItem("/_ViewImports.cshtml");
var viewImport2 = new TestRazorProjectItem("/Views/Home/_ViewImports.cshtml");
projectSystem.Add(viewImport1);
projectSystem.Add(viewImport2);
// Act
var items = projectSystem.FindHierarchicalItems("/", "/Views/Home/Index.cshtml", "_ViewImports.cshtml");
// Assert
Assert.Collection(
items,
item => Assert.Same(viewImport2, item),
item => Assert.False(item.Exists),
item => Assert.Same(viewImport1, item));
}
[Fact]
public void DirectoryNode_GetDirectory_ReturnsRoot()
{
// Arrange
var root = new DirectoryNode("/");
// Act
var result = root.GetDirectory("/");
// Assert
Assert.Same(root, result);
}
[Fact]
public void DirectoryNode_GetDirectory_ReturnsNull_IfDirectoryDoesNotExist()
{
// Arrange
var root = new DirectoryNode("/");
// Act
var result = root.GetDirectory("/does-not/exist");
// Assert
Assert.Null(result);
}
[Fact]
public void DirectoryNode_AddFile_CanAddToRoot()
{
// Arrange
var root = new DirectoryNode("/");
var projectItem = new TestRazorProjectItem("/File.txt");
// Act
root.AddFile(new FileNode("/File.txt", projectItem));
// Assert
Assert.Empty(root.Directories);
Assert.Collection(
root.Files,
file => Assert.Same(projectItem, file.ProjectItem));
}
[Fact]
public void DirectoryNode_AddFile_CanAddToNestedDirectory()
{
// Arrange
var root = new DirectoryNode("/");
var projectItem = new TestRazorProjectItem("/Pages/Shared/_Layout.cshtml");
// Act
root.AddFile(new FileNode("/Pages/Shared/_Layout.cshtml", projectItem));
// Assert
Assert.Collection(
root.Directories,
directory =>
{
Assert.Equal("/Pages/", directory.Path);
Assert.Empty(directory.Files);
Assert.Collection(
directory.Directories,
subDirectory =>
{
Assert.Equal("/Pages/Shared/", subDirectory.Path);
Assert.Collection(
subDirectory.Files,
file => Assert.Same(projectItem, file.ProjectItem));
});
});
}
[Fact]
public void DirectoryNode_AddMultipleFiles_ToSameDirectory()
{
// Arrange
var root = new DirectoryNode("/");
var projectItem1 = new TestRazorProjectItem("/Pages/Shared/_Layout.cshtml");
var projectItem2 = new TestRazorProjectItem("/Pages/Shared/_Partial.cshtml");
// Act
root.AddFile(new FileNode(projectItem1.FilePath, projectItem1));
root.AddFile(new FileNode(projectItem2.FilePath, projectItem2));
// Assert
Assert.Collection(
root.Directories,
directory =>
{
Assert.Equal("/Pages/", directory.Path);
Assert.Empty(directory.Files);
Assert.Collection(
directory.Directories,
subDirectory =>
{
Assert.Equal("/Pages/Shared/", subDirectory.Path);
Assert.Collection(
subDirectory.Files,
file => Assert.Same(projectItem1, file.ProjectItem),
file => Assert.Same(projectItem2, file.ProjectItem));
});
});
}
[Fact]
public void DirectoryNode_AddsFiles_ToSiblingDirectories()
{
// Arrange
var root = new DirectoryNode("/");
var projectItem1 = new TestRazorProjectItem("/Pages/Products/Index.cshtml");
var projectItem2 = new TestRazorProjectItem("/Pages/Accounts/About.cshtml");
// Act
root.AddFile(new FileNode(projectItem1.FilePath, projectItem1));
root.AddFile(new FileNode(projectItem2.FilePath, projectItem2));
// Assert
Assert.Collection(
root.Directories,
directory =>
{
Assert.Equal("/Pages/", directory.Path);
Assert.Empty(directory.Files);
Assert.Collection(
directory.Directories,
subDirectory =>
{
Assert.Equal("/Pages/Products/", subDirectory.Path);
Assert.Collection(
subDirectory.Files,
file => Assert.Same(projectItem1, file.ProjectItem));
},
subDirectory =>
{
Assert.Equal("/Pages/Accounts/", subDirectory.Path);
Assert.Collection(
subDirectory.Files,
file => Assert.Same(projectItem2, file.ProjectItem));
});
});
}
[Fact]
public void DirectoryNode_GetItem_ReturnsItemAtRoot()
{
// Arrange
var root = new DirectoryNode("/");
var projectItem = new TestRazorProjectItem("/_ViewStart.cshtml");
root.AddFile(new FileNode(projectItem.FilePath, projectItem));
// Act
var result = root.GetItem(projectItem.FilePath);
// Assert
Assert.Same(result, projectItem);
}
[Fact]
public void DirectoryNode_GetItem_WhenFilePathSharesSameNameAsSiblingDirectory()
{
// Arrange
var root = new DirectoryNode("/");
var projectItem1 = new TestRazorProjectItem("/Home.cshtml");
var projectItem2 = new TestRazorProjectItem("/Home/About.cshtml");
root.AddFile(new FileNode(projectItem1.FilePath, projectItem1));
root.AddFile(new FileNode(projectItem2.FilePath, projectItem2));
// Act
var result = root.GetItem(projectItem1.FilePath);
// Assert
Assert.Same(result, projectItem1);
}
[Fact]
public void DirectoryNode_GetItem_WhenFileNameIsSameAsDirectoryName()
{
// Arrange
var projectItem1 = new TestRazorProjectItem("/Home/Home.cshtml");
var projectItem2 = new TestRazorProjectItem("/Home/About.cshtml");
var root = new DirectoryNode("/")
{
Directories =
{
new DirectoryNode("/Home/")
{
Files =
{
new FileNode(projectItem1.FilePath, projectItem1),
new FileNode(projectItem2.FilePath, projectItem2),
}
}
},
};
// Act
var result = root.GetItem(projectItem1.FilePath);
// Assert
Assert.Same(result, projectItem1);
}
}
}

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Razor.Language
public override string RelativePhysicalPath { get; }
public override bool Exists => true;
public override bool Exists { get; } = true;
public string Content { get; set; } = "Default content";

View File

@ -0,0 +1,81 @@
// 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.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Tools
{
public class CompositeRazorProjectFileSystemTest
{
[Fact]
public void EnumerateItems_ReturnsResultsFromAllFileSystems()
{
// Arrange
var basePath = "base-path";
var file1 = new TestRazorProjectItem("file1");
var file2 = new TestRazorProjectItem("file2");
var file3 = new TestRazorProjectItem("file3");
var fileSystem1 = Mock.Of<RazorProjectFileSystem>(
f => f.EnumerateItems(basePath) == new[] { file1 });
var fileSystem2 = Mock.Of<RazorProjectFileSystem>(
f => f.EnumerateItems(basePath) == Enumerable.Empty<RazorProjectItem>());
var fileSystem3 = Mock.Of<RazorProjectFileSystem>(
f => f.EnumerateItems(basePath) == new[] { file2, file3, });
var compositeRazorProjectFileSystem = new CompositeRazorProjectFileSystem(new[] { fileSystem1, fileSystem2, fileSystem3 });
// Act
var result = compositeRazorProjectFileSystem.EnumerateItems(basePath);
// Assert
Assert.Equal(new[] { file1, file2, file3 }, result);
}
[Fact]
public void EnumerateItems_ReturnsEmptySequence_IfNoFileSystemReturnsResults()
{
// Arrange
var basePath = "base-path";
var fileSystem1 = Mock.Of<RazorProjectFileSystem>(
f => f.EnumerateItems(basePath) == Enumerable.Empty<RazorProjectItem>());
var fileSystem2 = Mock.Of<RazorProjectFileSystem>(
f => f.EnumerateItems(basePath) == Enumerable.Empty<RazorProjectItem>());
var compositeRazorProjectFileSystem = new CompositeRazorProjectFileSystem(new[] { fileSystem1, fileSystem2 });
// Act
var result = compositeRazorProjectFileSystem.EnumerateItems(basePath);
// Assert
Assert.Empty(result);
}
[Fact]
public void GetItem_ReturnsFirstInstanceThatExists()
{
// Arrange
var basePath = "base-path";
var filePath = "file-path";
var file1 = new NotFoundProjectItem(basePath, filePath);
var file2 = new TestRazorProjectItem(filePath);
RazorProjectItem nullItem = null;
var fileSystem1 = Mock.Of<RazorProjectFileSystem>(
f => f.GetItem(filePath) == file1);
var fileSystem2 = Mock.Of<RazorProjectFileSystem>(
f => f.GetItem(filePath) == nullItem);
var fileSystem3 = Mock.Of<RazorProjectFileSystem>(
f => f.GetItem(filePath) == file2);
var compositeRazorProjectFileSystem = new CompositeRazorProjectFileSystem(new[] { fileSystem1, fileSystem2, fileSystem3 });
// Act
var result = compositeRazorProjectFileSystem.GetItem(filePath);
// Assert
Assert.Same(file2, result);
}
}
}

View File

@ -6,6 +6,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.Tools\Microsoft.AspNetCore.Razor.Tools.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.Test.Common\Microsoft.AspNetCore.Razor.Test.Common.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -1,2 +1,3 @@
<Project>
<Import Project="RazorTest.Introspection.targets" />
</Project>

View File

@ -0,0 +1 @@
@(

View File

@ -0,0 +1 @@
@DateTime.UtcNow

View File

@ -0,0 +1 @@
@DateTime.UtcNow

View File

@ -0,0 +1 @@
@DateTime.UtcNow

View File

@ -0,0 +1,9 @@
<Project>
<Target Name="_IntrospectRazorGenerateWithTargetPath">
<Message Text="RazorGenerateWithTargetPath: %(RazorGenerateWithTargetPath.Identity) %(RazorGenerateWithTargetPath.TargetPath) %(RazorGenerateWithTargetPath.GeneratedOutput)" Importance="High" />
</Target>
<Target Name="_IntrospectRazorEmbeddedResources">
<Message Text="CompileResource: %(_RazorCoreCompileResourceInputs.Identity) %(_RazorCoreCompileResourceInputs.LogicalName)" Importance="High" />
</Target>
</Project>