diff --git a/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs b/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs
index 0a9280bac2..f9acd45ec8 100644
--- a/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs
+++ b/src/Microsoft.Blazor.Build/Cli/Commands/BuildRazorCommand.cs
@@ -47,6 +47,7 @@ namespace Microsoft.Blazor.Build.Cli.Commands
using (var outputWriter = new StreamWriter(outputFilePath.Value()))
{
var diagnostics = new RazorCompiler().CompileFiles(
+ sourceDirPathValue,
inputRazorFilePaths,
"Blazor", // TODO: Add required option for namespace
outputWriter,
diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs
index d11b9c58b2..6d59fdab34 100644
--- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs
+++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs
@@ -21,42 +21,73 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation
///
/// Writes C# source code representing Blazor components defined by Razor files.
///
- /// Paths to the input files.
- /// The namespace for the generated classes.
+ /// Path to a directory containing input files.
+ /// Paths to the input files relative to . The generated namespaces will be based on these relative paths.
+ /// The base namespace for the generated classes.
/// A to which C# source code will be written.
/// If not null, additional information will be written to this .
/// A collection of instances representing any warnings or errors that were encountered.
- public ICollection CompileFiles(IEnumerable inputPaths, string outputNamespace, TextWriter resultOutput, TextWriter verboseOutput)
+ public ICollection CompileFiles(
+ string inputRootPath,
+ IEnumerable inputPaths,
+ string baseNamespace,
+ TextWriter resultOutput,
+ TextWriter verboseOutput)
=> inputPaths.SelectMany(path =>
{
using (var reader = File.OpenText(path))
{
- return CompileSingleFile(path, reader, outputNamespace, resultOutput, verboseOutput);
+ return CompileSingleFile(inputRootPath, path, reader, baseNamespace, resultOutput, verboseOutput);
}
}).ToList();
///
/// Writes C# source code representing a Blazor component defined by a Razor file.
///
- /// The path to the input file.
- /// The namespace for the generated class.
+ /// Path to a directory containing input files.
+ /// Paths to the input files relative to . The generated namespaces will be based on these relative paths.
+ /// The base namespace for the generated class.
/// A to which C# source code will be written.
/// If not null, additional information will be written to this .
/// An enumerable of instances representing any warnings or errors that were encountered.
- public IEnumerable CompileSingleFile(string inputFilePath, TextReader inputFileReader, string outputNamespace, TextWriter resultOutput, TextWriter verboseOutput)
+ public IEnumerable CompileSingleFile(
+ string inputRootPath,
+ string inputFilePath,
+ TextReader inputFileReader,
+ string baseNamespace,
+ TextWriter resultOutput,
+ TextWriter verboseOutput)
{
+ if (inputFileReader == null)
+ {
+ throw new ArgumentNullException(nameof(inputFileReader));
+ }
+
if (resultOutput == null)
{
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
{
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($"public class {className}");
+ resultOutput.WriteLine($"namespace {combinedNamespace} {{");
+ resultOutput.WriteLine($"public class {itemClassName}");
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);
- if (!ClassNameRegex.IsMatch(basename))
+ // First represent inputFilePath as a path relative to inputRootPath. Not using Path.GetRelativePath
+ // 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);
}
}
}
diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs
index f8985451b7..9e50e3fdd3 100644
--- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs
+++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs
@@ -21,7 +21,8 @@ namespace Microsoft.Blazor.Build.Test
{
// Arrange/Act
var result = CompileToCSharp(
- "x:\\dir\\subdir\\Filename with spaces.cshtml",
+ "x:\\dir\\subdir",
+ "Filename with spaces.cshtml",
"ignored code",
"ignored namespace");
@@ -34,28 +35,55 @@ namespace Microsoft.Blazor.Build.Test
});
}
- [Fact]
- public void CreatesClassWithCorrectNameAndNamespace()
+ [Theory]
+ [InlineData("\\unrelated.cs")]
+ [InlineData("..\\outsideroot.cs")]
+ public void RejectsFilenameOutsideRoot(string filename)
{
// 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(
- "x:\\dir\\subdir\\Filename.cshtml",
+ "x:\\dir\\subdir",
+ relativePath,
"{* No code *}",
- "MyCompany.MyNamespace");
+ "Test.Base");
// Assert
Assert.Empty(result.Diagnostics);
Assert.Collection(result.Assembly.GetTypes(),
type =>
{
- Assert.Equal("Filename", type.Name);
- Assert.Equal("MyCompany.MyNamespace", type.Namespace);
+ Assert.Equal(expectedNamespace, 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())
{
var diagnosticsLog = string.Join(Environment.NewLine,
@@ -82,16 +110,17 @@ namespace Microsoft.Blazor.Build.Test
{
compilation.Emit(peStream);
+ var diagnostics = compilation.GetDiagnostics();
return new CompileToAssemblyResult
{
- Diagnostics = compilation.GetDiagnostics(),
+ Diagnostics = diagnostics,
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 resultWriter = new StreamWriter(resultStream))
@@ -100,7 +129,8 @@ namespace Microsoft.Blazor.Build.Test
using (var inputReader = new StringReader(cshtmlContent))
{
var diagnostics = new RazorCompiler().CompileSingleFile(
- cshtmlFilename,
+ cshtmlRootPath,
+ cshtmlRelativePath,
inputReader,
outputNamespace,
resultWriter,