When compiling Razor files, generate namespace based on directory path
This commit is contained in:
parent
654c16fb44
commit
a32b857d96
|
|
@ -47,6 +47,7 @@ namespace Microsoft.Blazor.Build.Cli.Commands
|
||||||
using (var outputWriter = new StreamWriter(outputFilePath.Value()))
|
using (var outputWriter = new StreamWriter(outputFilePath.Value()))
|
||||||
{
|
{
|
||||||
var diagnostics = new RazorCompiler().CompileFiles(
|
var diagnostics = new RazorCompiler().CompileFiles(
|
||||||
|
sourceDirPathValue,
|
||||||
inputRazorFilePaths,
|
inputRazorFilePaths,
|
||||||
"Blazor", // TODO: Add required option for namespace
|
"Blazor", // TODO: Add required option for namespace
|
||||||
outputWriter,
|
outputWriter,
|
||||||
|
|
|
||||||
|
|
@ -21,42 +21,73 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes C# source code representing Blazor components defined by Razor files.
|
/// Writes C# source code representing Blazor components defined by Razor files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputPaths">Paths to the input files.</param>
|
/// <param name="inputRootPath">Path to a directory containing input files.</param>
|
||||||
/// <param name="outputNamespace">The namespace for the generated classes.</param>
|
/// <param name="inputPaths">Paths to the input files relative to <paramref name="inputRootPath"/>. The generated namespaces will be based on these relative paths.</param>
|
||||||
|
/// <param name="baseNamespace">The base namespace for the generated classes.</param>
|
||||||
/// <param name="resultOutput">A <see cref="TextWriter"/> to which C# source code will be written.</param>
|
/// <param name="resultOutput">A <see cref="TextWriter"/> to which C# source code will be written.</param>
|
||||||
/// <param name="verboseOutput">If not null, additional information will be written to this <see cref="TextWriter"/>.</param>
|
/// <param name="verboseOutput">If not null, additional information will be written to this <see cref="TextWriter"/>.</param>
|
||||||
/// <returns>A collection of <see cref="RazorCompilerDiagnostic"/> instances representing any warnings or errors that were encountered.</returns>
|
/// <returns>A collection of <see cref="RazorCompilerDiagnostic"/> instances representing any warnings or errors that were encountered.</returns>
|
||||||
public ICollection<RazorCompilerDiagnostic> CompileFiles(IEnumerable<string> inputPaths, string outputNamespace, TextWriter resultOutput, TextWriter verboseOutput)
|
public ICollection<RazorCompilerDiagnostic> CompileFiles(
|
||||||
|
string inputRootPath,
|
||||||
|
IEnumerable<string> inputPaths,
|
||||||
|
string baseNamespace,
|
||||||
|
TextWriter resultOutput,
|
||||||
|
TextWriter verboseOutput)
|
||||||
=> inputPaths.SelectMany(path =>
|
=> inputPaths.SelectMany(path =>
|
||||||
{
|
{
|
||||||
using (var reader = File.OpenText(path))
|
using (var reader = File.OpenText(path))
|
||||||
{
|
{
|
||||||
return CompileSingleFile(path, reader, outputNamespace, resultOutput, verboseOutput);
|
return CompileSingleFile(inputRootPath, path, reader, baseNamespace, resultOutput, verboseOutput);
|
||||||
}
|
}
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes C# source code representing a Blazor component defined by a Razor file.
|
/// Writes C# source code representing a Blazor component defined by a Razor file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputPaths">The path to the input file.</param>
|
/// <param name="inputRootPath">Path to a directory containing input files.</param>
|
||||||
/// <param name="outputNamespace">The namespace for the generated class.</param>
|
/// <param name="inputPaths">Paths to the input files relative to <paramref name="inputRootPath"/>. The generated namespaces will be based on these relative paths.</param>
|
||||||
|
/// <param name="baseNamespace">The base namespace for the generated class.</param>
|
||||||
/// <param name="resultOutput">A <see cref="TextWriter"/> to which C# source code will be written.</param>
|
/// <param name="resultOutput">A <see cref="TextWriter"/> to which C# source code will be written.</param>
|
||||||
/// <param name="verboseOutput">If not null, additional information will be written to this <see cref="TextWriter"/>.</param>
|
/// <param name="verboseOutput">If not null, additional information will be written to this <see cref="TextWriter"/>.</param>
|
||||||
/// <returns>An enumerable of <see cref="RazorCompilerDiagnostic"/> instances representing any warnings or errors that were encountered.</returns>
|
/// <returns>An enumerable of <see cref="RazorCompilerDiagnostic"/> instances representing any warnings or errors that were encountered.</returns>
|
||||||
public IEnumerable<RazorCompilerDiagnostic> CompileSingleFile(string inputFilePath, TextReader inputFileReader, string outputNamespace, TextWriter resultOutput, TextWriter verboseOutput)
|
public IEnumerable<RazorCompilerDiagnostic> CompileSingleFile(
|
||||||
|
string inputRootPath,
|
||||||
|
string inputFilePath,
|
||||||
|
TextReader inputFileReader,
|
||||||
|
string baseNamespace,
|
||||||
|
TextWriter resultOutput,
|
||||||
|
TextWriter verboseOutput)
|
||||||
{
|
{
|
||||||
|
if (inputFileReader == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(inputFileReader));
|
||||||
|
}
|
||||||
|
|
||||||
if (resultOutput == null)
|
if (resultOutput == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(resultOutput));
|
throw new ArgumentNullException(nameof(resultOutput));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(inputRootPath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null or empty.", nameof(inputRootPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(baseNamespace))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null or empty.", nameof(baseNamespace));
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
verboseOutput?.WriteLine($"Compiling {inputFilePath}...");
|
verboseOutput?.WriteLine($"Compiling {inputFilePath}...");
|
||||||
var className = GetClassName(inputFilePath);
|
var (itemNamespace, itemClassName) = GetNamespaceAndClassName(inputRootPath, inputFilePath);
|
||||||
|
var combinedNamespace = string.IsNullOrEmpty(itemNamespace)
|
||||||
|
? baseNamespace
|
||||||
|
: $"{baseNamespace}.{itemNamespace}";
|
||||||
|
|
||||||
resultOutput.WriteLine($"namespace {outputNamespace} {{");
|
resultOutput.WriteLine($"namespace {combinedNamespace} {{");
|
||||||
resultOutput.WriteLine($"public class {className}");
|
resultOutput.WriteLine($"public class {itemClassName}");
|
||||||
resultOutput.WriteLine("{");
|
resultOutput.WriteLine("{");
|
||||||
resultOutput.WriteLine("}");
|
resultOutput.WriteLine("}");
|
||||||
resultOutput.WriteLine("}");
|
resultOutput.WriteLine("}");
|
||||||
|
|
@ -82,15 +113,32 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetClassName(string inputFilePath)
|
private static (string, string) GetNamespaceAndClassName(string inputRootPath, string inputFilePath)
|
||||||
{
|
{
|
||||||
var basename = Path.GetFileNameWithoutExtension(inputFilePath);
|
// First represent inputFilePath as a path relative to inputRootPath. Not using Path.GetRelativePath
|
||||||
if (!ClassNameRegex.IsMatch(basename))
|
// because it doesn't handle cases like inputFilePath="\\something.cs".
|
||||||
|
var inputFilePathAbsolute = Path.GetFullPath(Path.Combine(inputRootPath, inputFilePath));
|
||||||
|
var inputRootPathWithTrailingSeparator = inputRootPath.EndsWith(Path.DirectorySeparatorChar)
|
||||||
|
? inputRootPath
|
||||||
|
: (inputRootPath + Path.DirectorySeparatorChar);
|
||||||
|
var inputFilePathRelative = inputFilePathAbsolute.StartsWith(inputRootPathWithTrailingSeparator)
|
||||||
|
? inputFilePathAbsolute.Substring(inputRootPathWithTrailingSeparator.Length)
|
||||||
|
: throw new RazorCompilerException($"File is not within source root directory: '{inputFilePath}'");
|
||||||
|
|
||||||
|
// Use the set of directory names in the relative path as namespace
|
||||||
|
var inputDirname = Path.GetDirectoryName(inputFilePathRelative);
|
||||||
|
var resultNamespace = inputDirname
|
||||||
|
.Replace(Path.DirectorySeparatorChar, '.')
|
||||||
|
.Replace(" ", string.Empty);
|
||||||
|
|
||||||
|
// Use the filename as class name
|
||||||
|
var inputBasename = Path.GetFileNameWithoutExtension(inputFilePathRelative);
|
||||||
|
if (!ClassNameRegex.IsMatch(inputBasename))
|
||||||
{
|
{
|
||||||
throw new RazorCompilerException($"Invalid name '{basename}'. The name must be valid for a C# class name.");
|
throw new RazorCompilerException($"Invalid name '{inputBasename}'. The name must be valid for a C# class name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return basename;
|
return (resultNamespace, inputBasename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
{
|
{
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
var result = CompileToCSharp(
|
var result = CompileToCSharp(
|
||||||
"x:\\dir\\subdir\\Filename with spaces.cshtml",
|
"x:\\dir\\subdir",
|
||||||
|
"Filename with spaces.cshtml",
|
||||||
"ignored code",
|
"ignored code",
|
||||||
"ignored namespace");
|
"ignored namespace");
|
||||||
|
|
||||||
|
|
@ -34,28 +35,55 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void CreatesClassWithCorrectNameAndNamespace()
|
[InlineData("\\unrelated.cs")]
|
||||||
|
[InlineData("..\\outsideroot.cs")]
|
||||||
|
public void RejectsFilenameOutsideRoot(string filename)
|
||||||
{
|
{
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
|
var result = CompileToCSharp(
|
||||||
|
"x:\\dir\\subdir",
|
||||||
|
filename,
|
||||||
|
"ignored code",
|
||||||
|
"ignored namespace");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(result.Diagnostics,
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal(RazorCompilerDiagnostic.DiagnosticType.Error, item.Type);
|
||||||
|
Assert.StartsWith($"File is not within source root directory: '{filename}'", item.Message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("ItemAtRoot.cs", "Test.Base", "ItemAtRoot")]
|
||||||
|
[InlineData(".\\ItemAtRoot.cs", "Test.Base", "ItemAtRoot")]
|
||||||
|
[InlineData("x:\\dir\\subdir\\ItemAtRoot.cs", "Test.Base", "ItemAtRoot")]
|
||||||
|
[InlineData("Dir1\\MyFile.cs", "Test.Base.Dir1", "MyFile")]
|
||||||
|
[InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Base.Dir1.Dir2", "MyFile")]
|
||||||
|
public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName)
|
||||||
|
{
|
||||||
|
// Arrange/Acts
|
||||||
var result = CompileToAssembly(
|
var result = CompileToAssembly(
|
||||||
"x:\\dir\\subdir\\Filename.cshtml",
|
"x:\\dir\\subdir",
|
||||||
|
relativePath,
|
||||||
"{* No code *}",
|
"{* No code *}",
|
||||||
"MyCompany.MyNamespace");
|
"Test.Base");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result.Diagnostics);
|
Assert.Empty(result.Diagnostics);
|
||||||
Assert.Collection(result.Assembly.GetTypes(),
|
Assert.Collection(result.Assembly.GetTypes(),
|
||||||
type =>
|
type =>
|
||||||
{
|
{
|
||||||
Assert.Equal("Filename", type.Name);
|
Assert.Equal(expectedNamespace, type.Namespace);
|
||||||
Assert.Equal("MyCompany.MyNamespace", type.Namespace);
|
Assert.Equal(expectedClassName, type.Name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompileToAssemblyResult CompileToAssembly(string cshtmlFilename, string cshtmlContent, string outputNamespace)
|
private static CompileToAssemblyResult CompileToAssembly(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace)
|
||||||
{
|
{
|
||||||
var csharpResult = CompileToCSharp(cshtmlFilename, cshtmlContent, outputNamespace);
|
var csharpResult = CompileToCSharp(cshtmlRootPath, cshtmlRelativePath, cshtmlContent, outputNamespace);
|
||||||
if (csharpResult.Diagnostics.Any())
|
if (csharpResult.Diagnostics.Any())
|
||||||
{
|
{
|
||||||
var diagnosticsLog = string.Join(Environment.NewLine,
|
var diagnosticsLog = string.Join(Environment.NewLine,
|
||||||
|
|
@ -82,16 +110,17 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
{
|
{
|
||||||
compilation.Emit(peStream);
|
compilation.Emit(peStream);
|
||||||
|
|
||||||
|
var diagnostics = compilation.GetDiagnostics();
|
||||||
return new CompileToAssemblyResult
|
return new CompileToAssemblyResult
|
||||||
{
|
{
|
||||||
Diagnostics = compilation.GetDiagnostics(),
|
Diagnostics = diagnostics,
|
||||||
VerboseLog = csharpResult.VerboseLog,
|
VerboseLog = csharpResult.VerboseLog,
|
||||||
Assembly = Assembly.Load(peStream.ToArray())
|
Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompileToCSharpResult CompileToCSharp(string cshtmlFilename, string cshtmlContent, string outputNamespace)
|
private static CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace)
|
||||||
{
|
{
|
||||||
using (var resultStream = new MemoryStream())
|
using (var resultStream = new MemoryStream())
|
||||||
using (var resultWriter = new StreamWriter(resultStream))
|
using (var resultWriter = new StreamWriter(resultStream))
|
||||||
|
|
@ -100,7 +129,8 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
using (var inputReader = new StringReader(cshtmlContent))
|
using (var inputReader = new StringReader(cshtmlContent))
|
||||||
{
|
{
|
||||||
var diagnostics = new RazorCompiler().CompileSingleFile(
|
var diagnostics = new RazorCompiler().CompileSingleFile(
|
||||||
cshtmlFilename,
|
cshtmlRootPath,
|
||||||
|
cshtmlRelativePath,
|
||||||
inputReader,
|
inputReader,
|
||||||
outputNamespace,
|
outputNamespace,
|
||||||
resultWriter,
|
resultWriter,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue