Get rid of RazorCompiler
Adds a little more use of Razor extensibility. Razor is a plugin model, so we can't be the 'first mover' for initiating compilation in the build tools and IDE. Reorganizes tests and fills out more reusable test infrastructure for Razor-driven testing. Adds tests for declaration-only configuration.
This commit is contained in:
parent
daf6a404f9
commit
6182e8448d
|
|
@ -1,13 +1,13 @@
|
|||
// 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 Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
||||
using Microsoft.AspNetCore.Blazor.Razor;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Razor;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
|
||||
{
|
||||
|
|
@ -47,24 +47,35 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
|
|||
return 1;
|
||||
}
|
||||
|
||||
var inputRazorFilePaths = FindRazorFiles(sourceDirPathValue).ToList();
|
||||
var fileSystem = RazorProjectFileSystem.Create(sourceDirPathValue);
|
||||
var engine = RazorProjectEngine.Create(BlazorExtensionInitializer.DefaultConfiguration, fileSystem, b =>
|
||||
{
|
||||
BlazorExtensionInitializer.Register(b);
|
||||
});
|
||||
|
||||
var diagnostics = new List<RazorDiagnostic>();
|
||||
var sourceFiles = FindRazorFiles(sourceDirPathValue).ToList();
|
||||
using (var outputWriter = new StreamWriter(outputFilePath.Value()))
|
||||
{
|
||||
var diagnostics = new RazorCompiler().CompileFiles(
|
||||
sourceDirPathValue,
|
||||
inputRazorFilePaths,
|
||||
baseNamespace.Value(),
|
||||
outputWriter,
|
||||
verboseFlag.HasValue() ? Console.Out : null);
|
||||
|
||||
foreach (var diagnostic in diagnostics)
|
||||
foreach (var sourceFile in sourceFiles)
|
||||
{
|
||||
Console.WriteLine(diagnostic.FormatForConsole());
|
||||
}
|
||||
var item = fileSystem.GetItem(sourceFile);
|
||||
|
||||
var hasError = diagnostics.Any(item => item.Type == RazorCompilerDiagnostic.DiagnosticType.Error);
|
||||
return hasError ? 1 : 0;
|
||||
var codeDocument = engine.Process(item);
|
||||
var cSharpDocument = codeDocument.GetCSharpDocument();
|
||||
|
||||
outputWriter.WriteLine(cSharpDocument.GeneratedCode);
|
||||
diagnostics.AddRange(cSharpDocument.Diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var diagnostic in diagnostics)
|
||||
{
|
||||
Console.WriteLine(diagnostic.ToString());
|
||||
}
|
||||
|
||||
var hasError = diagnostics.Any(item => item.Severity == RazorDiagnosticSeverity.Error);
|
||||
return hasError ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
|
||||
{
|
||||
internal class BlazorProjectItem : RazorProjectItem
|
||||
{
|
||||
private readonly string _projectBasePath;
|
||||
private readonly string _itemFullPhysicalPath;
|
||||
private readonly Stream _itemContents;
|
||||
|
||||
public BlazorProjectItem(
|
||||
string projectBasePath,
|
||||
string itemFullPhysicalPath,
|
||||
Stream itemFileContents)
|
||||
{
|
||||
_projectBasePath = projectBasePath;
|
||||
_itemFullPhysicalPath = itemFullPhysicalPath;
|
||||
_itemContents = itemFileContents;
|
||||
}
|
||||
|
||||
public override string BasePath => _projectBasePath;
|
||||
|
||||
public override string FilePath => _itemFullPhysicalPath;
|
||||
|
||||
public override string PhysicalPath => _itemFullPhysicalPath;
|
||||
|
||||
public override bool Exists => true;
|
||||
|
||||
public override Stream Read() => _itemContents;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.CodeDom.Compiler;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Razor;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides facilities for transforming Razor files into Blazor component classes.
|
||||
/// </summary>
|
||||
public class RazorCompiler
|
||||
{
|
||||
private static CodeDomProvider _csharpCodeDomProvider = CodeDomProvider.CreateProvider("c#");
|
||||
|
||||
/// <summary>
|
||||
/// Writes C# source code representing Blazor components defined by Razor files.
|
||||
/// </summary>
|
||||
/// <param name="inputRootPath">Path to a directory containing input files.</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="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>
|
||||
public ICollection<RazorCompilerDiagnostic> CompileFiles(
|
||||
string inputRootPath,
|
||||
IEnumerable<string> inputPaths,
|
||||
string baseNamespace,
|
||||
TextWriter resultOutput,
|
||||
TextWriter verboseOutput)
|
||||
=> inputPaths.SelectMany(path =>
|
||||
{
|
||||
using (var reader = File.OpenRead(path))
|
||||
{
|
||||
return CompileSingleFile(inputRootPath, path, reader, baseNamespace, resultOutput, verboseOutput);
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Writes C# source code representing a Blazor component defined by a Razor file.
|
||||
/// </summary>
|
||||
/// <param name="inputRootPath">Path to a directory containing input files.</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="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>
|
||||
public IEnumerable<RazorCompilerDiagnostic> CompileSingleFile(
|
||||
string inputRootPath,
|
||||
string inputFilePath,
|
||||
Stream inputFileContents,
|
||||
string baseNamespace,
|
||||
TextWriter resultOutput,
|
||||
TextWriter verboseOutput)
|
||||
{
|
||||
if (inputFileContents == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inputFileContents));
|
||||
}
|
||||
|
||||
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 (itemNamespace, itemClassName) = GetNamespaceAndClassName(inputRootPath, inputFilePath);
|
||||
var combinedNamespace = string.IsNullOrEmpty(itemNamespace)
|
||||
? baseNamespace
|
||||
: $"{baseNamespace}.{itemNamespace}";
|
||||
|
||||
// TODO: Pass through info about whether this is a design-time build, and if so,
|
||||
// just emit enough of a stub class that intellisense will show the correct type
|
||||
// name and any public members. Don't need to actually emit all the RenderTreeBuilder
|
||||
// invocations.
|
||||
|
||||
var engine = RazorEngine.Create(b => BlazorExtensionInitializer.Register(b));
|
||||
var blazorTemplateEngine = new BlazorTemplateEngine(
|
||||
engine,
|
||||
RazorProjectFileSystem.Create(inputRootPath));
|
||||
var codeDoc = blazorTemplateEngine.CreateCodeDocument(
|
||||
new BlazorProjectItem(inputRootPath, inputFilePath, inputFileContents));
|
||||
codeDoc.Items[BlazorCodeDocItems.Namespace] = combinedNamespace;
|
||||
codeDoc.Items[BlazorCodeDocItems.ClassName] = itemClassName;
|
||||
var csharpDocument = blazorTemplateEngine.GenerateCode(codeDoc);
|
||||
var generatedCode = csharpDocument.GeneratedCode;
|
||||
|
||||
// Add parameters to the primary method via string manipulation because
|
||||
// DefaultDocumentWriter's VisitMethodDeclaration can't emit parameters
|
||||
var primaryMethodSource = $"protected override void {BlazorComponent.BuildRenderTreeMethodName}";
|
||||
generatedCode = generatedCode.Replace(
|
||||
$"{primaryMethodSource}()",
|
||||
$"{primaryMethodSource}({typeof(RenderTreeBuilder).FullName} builder)");
|
||||
|
||||
resultOutput.WriteLine(generatedCode);
|
||||
|
||||
return Enumerable.Empty<RazorCompilerDiagnostic>();
|
||||
}
|
||||
catch (RazorCompilerException ex)
|
||||
{
|
||||
return new[] { ex.ToDiagnostic(inputFilePath) };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new RazorCompilerDiagnostic(
|
||||
RazorCompilerDiagnostic.DiagnosticType.Error,
|
||||
inputFilePath,
|
||||
1,
|
||||
1,
|
||||
$"Unexpected exception: {ex.Message}{Environment.NewLine}{ex.StackTrace}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static (string, string) GetNamespaceAndClassName(string inputRootPath, string inputFilePath)
|
||||
{
|
||||
// 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 (!IsValidClassName(inputBasename))
|
||||
{
|
||||
throw new RazorCompilerException($"Invalid name '{inputBasename}'. The name must be valid for a C# class name.");
|
||||
}
|
||||
|
||||
return (resultNamespace, inputBasename);
|
||||
}
|
||||
|
||||
private static bool IsValidClassName(string name)
|
||||
=> _csharpCodeDomProvider.IsValidIdentifier(name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class BlazorRazorCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument)
|
||||
{
|
||||
var documentNode = codeDocument.GetDocumentIntermediateNode();
|
||||
ThrowForMissingDocumentDependency(documentNode);
|
||||
|
||||
var writer = new DocumentWriterWorkaround().Create(documentNode.Target, documentNode.Options);
|
||||
try
|
||||
{
|
||||
var cSharpDocument = writer.WriteDocument(codeDocument, documentNode);
|
||||
codeDocument.SetCSharpDocument(cSharpDocument);
|
||||
}
|
||||
catch (RazorCompilerException ex)
|
||||
{
|
||||
// Currently the Blazor code generation has some 'fatal errors' that can cause code generation
|
||||
// to fail completely. This class is here to make that implementation work gracefully.
|
||||
var cSharpDocument = RazorCSharpDocument.Create("", documentNode.Options, new[] { ex.Diagnostic });
|
||||
codeDocument.SetCSharpDocument(cSharpDocument);
|
||||
}
|
||||
}
|
||||
|
||||
private class DocumentWriterWorkaround : DocumentWriter
|
||||
{
|
||||
public override RazorCSharpDocument WriteDocument(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// 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 AngleSharp;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal static class BlazorDiagnosticFactory
|
||||
{
|
||||
public static readonly RazorDiagnosticDescriptor InvalidComponentAttributeSyntax = new RazorDiagnosticDescriptor(
|
||||
"BL9980",
|
||||
() => "Wrong syntax for '{0}' on '{1}': As a temporary " +
|
||||
$"limitation, component attributes must be expressed with C# syntax. For example, " +
|
||||
$"SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" is not.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic Create_InvalidComponentAttributeSynx(TextPosition position, SourceSpan? span, string attributeName, string componentName)
|
||||
{
|
||||
span = CalculateSourcePosition(span, position);
|
||||
return RazorDiagnostic.Create(InvalidComponentAttributeSyntax, span ?? SourceSpan.Undefined, attributeName, componentName);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor UnexpectedClosingTag = new RazorDiagnosticDescriptor(
|
||||
"BL9981",
|
||||
() => "Unexpected closing tag '{0}' with no matching start tag.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic Create_UnexpectedClosingTag(SourceSpan? span, string tagName)
|
||||
{
|
||||
return RazorDiagnostic.Create(UnexpectedClosingTag, span ?? SourceSpan.Undefined, tagName);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor MismatchedClosingTag = new RazorDiagnosticDescriptor(
|
||||
"BL9982",
|
||||
() => "Mismatching closing tag. Found '{0}' but expected '{1}'.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic Create_MismatchedClosingTag(SourceSpan? span, string expectedTagName, string tagName)
|
||||
{
|
||||
return RazorDiagnostic.Create(MismatchedClosingTag, span ?? SourceSpan.Undefined, expectedTagName, tagName);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor MismatchedClosingTagKind = new RazorDiagnosticDescriptor(
|
||||
"BL9984",
|
||||
() => "Mismatching closing tag. Found '{0}' of type '{1}' but expected type '{2}'.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic Create_MismatchedClosingTagKind(SourceSpan? span, string tagName, string kind, string expectedKind)
|
||||
{
|
||||
return RazorDiagnostic.Create(MismatchedClosingTagKind, span ?? SourceSpan.Undefined, tagName, kind, expectedKind);
|
||||
}
|
||||
|
||||
private static SourceSpan? CalculateSourcePosition(
|
||||
SourceSpan? razorTokenPosition,
|
||||
TextPosition htmlNodePosition)
|
||||
{
|
||||
if (razorTokenPosition.HasValue)
|
||||
{
|
||||
var razorPos = razorTokenPosition.Value;
|
||||
return new SourceSpan(
|
||||
razorPos.FilePath,
|
||||
razorPos.AbsoluteIndex + htmlNodePosition.Position,
|
||||
razorPos.LineIndex + htmlNodePosition.Line - 1,
|
||||
htmlNodePosition.Line == 1
|
||||
? razorPos.CharacterIndex + htmlNodePosition.Column - 1
|
||||
: htmlNodePosition.Column - 1,
|
||||
length: 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
|
||||
|
|
@ -10,6 +11,38 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
public class BlazorExtensionInitializer : RazorExtensionInitializer
|
||||
{
|
||||
public static readonly RazorConfiguration DeclarationConfiguration;
|
||||
|
||||
public static readonly RazorConfiguration DefaultConfiguration;
|
||||
|
||||
static BlazorExtensionInitializer()
|
||||
{
|
||||
// RazorConfiguration is changing between 15.7 and preview2 builds of Razor, this is a reflection-based
|
||||
// workaround.
|
||||
DeclarationConfiguration = Create("BlazorDeclaration-0.1");
|
||||
DefaultConfiguration = Create("Blazor-0.1");
|
||||
|
||||
RazorConfiguration Create(string configurationName)
|
||||
{
|
||||
var args = new object[] { RazorLanguageVersion.Version_2_1, configurationName, Array.Empty<RazorExtension>(), };
|
||||
|
||||
MethodInfo method;
|
||||
ConstructorInfo constructor;
|
||||
if ((method = typeof(RazorConfiguration).GetMethod("Create", BindingFlags.Public | BindingFlags.Static)) != null)
|
||||
{
|
||||
return (RazorConfiguration)method.Invoke(null, args);
|
||||
}
|
||||
else if ((constructor = typeof(RazorConfiguration).GetConstructors().FirstOrDefault()) != null)
|
||||
{
|
||||
return (RazorConfiguration)constructor.Invoke(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Can't create a configuration. This is bad.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
|
|
@ -26,29 +59,21 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
|
||||
builder.Features.Add(new BlazorImportProjectFeature());
|
||||
|
||||
var index = builder.Phases.IndexOf(builder.Phases.OfType<IRazorCSharpLoweringPhase>().Single());
|
||||
builder.Phases[index] = new BlazorRazorCSharpLoweringPhase();
|
||||
|
||||
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
|
||||
|
||||
builder.Features.Add(new ComponentDocumentClassifierPass());
|
||||
}
|
||||
|
||||
// This is temporarily used to initialize a RazorEngine by the build tools until we get the features
|
||||
// we need into the RazorProjectEngine (namespace).
|
||||
public static void Register(IRazorEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
|
||||
|
||||
if (builder.Configuration.ConfigurationName == DeclarationConfiguration.ConfigurationName)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
// This is for 'declaration only' processing. We don't want to try and emit any method bodies during
|
||||
// the design time build because we can't do it correctly until the set of components is known.
|
||||
builder.Features.Add(new EliminateMethodBodyPass());
|
||||
}
|
||||
|
||||
FunctionsDirective.Register(builder);
|
||||
ImplementsDirective.Register(builder);
|
||||
InheritsDirective.Register(builder);
|
||||
InjectDirective.Register(builder);
|
||||
LayoutDirective.Register(builder);
|
||||
|
||||
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
|
||||
|
||||
builder.Features.Add(new ComponentDocumentClassifierPass());
|
||||
}
|
||||
|
||||
public override void Initialize(RazorProjectEngineBuilder builder)
|
||||
|
|
|
|||
|
|
@ -253,7 +253,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
if (isComponent && nextTag.Attributes.Count > 0)
|
||||
{
|
||||
ThrowTemporaryComponentSyntaxError(node, nextTag, tagNameOriginalCase);
|
||||
var diagnostic = BlazorDiagnosticFactory.Create_InvalidComponentAttributeSynx(
|
||||
nextTag.Position,
|
||||
node.Source,
|
||||
nextTag.Attributes[0].Key,
|
||||
tagNameOriginalCase);
|
||||
throw new RazorCompilerException(diagnostic);
|
||||
}
|
||||
|
||||
foreach (var attribute in nextTag.Attributes)
|
||||
|
|
@ -368,13 +373,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
}
|
||||
}
|
||||
|
||||
private void ThrowTemporaryComponentSyntaxError(HtmlContentIntermediateNode node, HtmlTagToken tag, string componentName)
|
||||
=> throw new RazorCompilerException(
|
||||
$"Wrong syntax for '{tag.Attributes[0].Key}' on '{componentName}': As a temporary " +
|
||||
$"limitation, component attributes must be expressed with C# syntax. For example, " +
|
||||
$"SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" is not.",
|
||||
CalculateSourcePosition(node.Source, tag.Position));
|
||||
|
||||
private SourceSpan? CalculateSourcePosition(
|
||||
SourceSpan? razorTokenPosition,
|
||||
TextPosition htmlNodePosition)
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="RazorTemplateEngine"/> for Blazor components.
|
||||
/// </summary>
|
||||
public class BlazorTemplateEngine : RazorTemplateEngine
|
||||
{
|
||||
// We need to implement and register this feature for tooling support to work. Subclassing TemplateEngine
|
||||
// doesn't work inside visual studio.
|
||||
private readonly BlazorImportProjectFeature _feature;
|
||||
|
||||
public BlazorTemplateEngine(RazorEngine engine, RazorProject project)
|
||||
: base(engine, project)
|
||||
{
|
||||
_feature = new BlazorImportProjectFeature();
|
||||
|
||||
Options.DefaultImports = RazorSourceDocument.ReadFrom(_feature.DefaultImports);
|
||||
}
|
||||
|
||||
public override IEnumerable<RazorProjectItem> GetImportItems(RazorProjectItem projectItem)
|
||||
{
|
||||
if (projectItem == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(projectItem));
|
||||
}
|
||||
|
||||
return _feature.GetHierarchicalImports(Project, projectItem);
|
||||
}
|
||||
|
||||
private static RazorSourceDocument GetDefaultImports()
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8))
|
||||
{
|
||||
// TODO: Add other commonly-used Blazor namespaces here. Can't do so yet
|
||||
// because the tooling wouldn't know about it, so it would still look like
|
||||
// an error if you hadn't explicitly imported them.
|
||||
writer.WriteLine("@using System");
|
||||
writer.WriteLine("@using System.Collections.Generic");
|
||||
writer.WriteLine("@using System.Linq");
|
||||
writer.WriteLine("@using System.Threading.Tasks");
|
||||
writer.Flush();
|
||||
|
||||
stream.Position = 0;
|
||||
return RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// 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.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
// Copied from the Razor repo
|
||||
internal static class CSharpIdentifier
|
||||
{
|
||||
private const string CshtmlExtension = ".cshtml";
|
||||
|
||||
public static string GetClassNameFromPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
if (path.EndsWith(CshtmlExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = path.Substring(0, path.Length - CshtmlExtension.Length);
|
||||
}
|
||||
|
||||
return SanitizeClassName(path);
|
||||
}
|
||||
|
||||
// CSharp Spec §2.4.2
|
||||
private static bool IsIdentifierStart(char character)
|
||||
{
|
||||
return char.IsLetter(character) ||
|
||||
character == '_' ||
|
||||
CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber;
|
||||
}
|
||||
|
||||
public static bool IsIdentifierPart(char character)
|
||||
{
|
||||
return char.IsDigit(character) ||
|
||||
IsIdentifierStart(character) ||
|
||||
IsIdentifierPartByUnicodeCategory(character);
|
||||
}
|
||||
|
||||
private static bool IsIdentifierPartByUnicodeCategory(char character)
|
||||
{
|
||||
var category = CharUnicodeInfo.GetUnicodeCategory(character);
|
||||
|
||||
return category == UnicodeCategory.NonSpacingMark || // Mn
|
||||
category == UnicodeCategory.SpacingCombiningMark || // Mc
|
||||
category == UnicodeCategory.ConnectorPunctuation || // Pc
|
||||
category == UnicodeCategory.Format; // Cf
|
||||
}
|
||||
|
||||
public static string SanitizeClassName(string inputName)
|
||||
{
|
||||
if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0]))
|
||||
{
|
||||
inputName = "_" + inputName;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(inputName.Length);
|
||||
for (var i = 0; i < inputName.Length; i++)
|
||||
{
|
||||
var ch = inputName[i];
|
||||
builder.Append(IsIdentifierPart(ch) ? ch : '_');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,34 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class ComponentDocumentClassifierPass : DocumentClassifierPassBase, IRazorDocumentClassifierPass
|
||||
public class ComponentDocumentClassifierPass : DocumentClassifierPassBase, IRazorDocumentClassifierPass
|
||||
{
|
||||
public static readonly string ComponentDocumentKind = "Blazor.Component-0.1";
|
||||
|
||||
private static readonly char[] PathSeparators = new char[] { '/', '\\' };
|
||||
|
||||
// This is a fallback value and will only be used if we can't compute
|
||||
// a reasonable namespace.
|
||||
public string BaseNamespace { get; set; } = "__BlazorGenerated";
|
||||
|
||||
// Set to true in the IDE so we can generated mangled class names. This is needed
|
||||
// to avoid conflicts between generated design-time code and the code in the editor.
|
||||
//
|
||||
// A better workaround for this would be to create a singlefilegenerator that overrides
|
||||
// the codegen process when a document is open, but this is more involved, so hacking
|
||||
// it for now.
|
||||
public bool MangleClassNames { get; set; } = false;
|
||||
|
||||
protected override string DocumentKind => ComponentDocumentKind;
|
||||
|
||||
protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
|
|
@ -28,24 +43,27 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
ClassDeclarationIntermediateNode @class,
|
||||
MethodDeclarationIntermediateNode method)
|
||||
{
|
||||
@namespace.Content = (string)codeDocument.Items[BlazorCodeDocItems.Namespace];
|
||||
if (@namespace.Content == null)
|
||||
if (!TryComputeNamespaceAndClass(
|
||||
codeDocument.Source.FilePath,
|
||||
codeDocument.Source.RelativePath,
|
||||
out var computedNamespace,
|
||||
out var computedClass))
|
||||
{
|
||||
@namespace.Content = "Blazor";
|
||||
// If we can't compute a nice namespace (no relative path) then just generate something
|
||||
// mangled.
|
||||
computedNamespace = BaseNamespace;
|
||||
computedClass = CSharpIdentifier.GetClassNameFromPath(codeDocument.Source.FilePath) ?? "__BlazorComponent";
|
||||
}
|
||||
|
||||
if (MangleClassNames)
|
||||
{
|
||||
computedClass = "__" + computedClass;
|
||||
}
|
||||
|
||||
@namespace.Content = computedNamespace;
|
||||
|
||||
@class.BaseType = BlazorApi.BlazorComponent.FullTypeName;
|
||||
@class.ClassName = (string)codeDocument.Items[BlazorCodeDocItems.ClassName];
|
||||
if (@class.ClassName == null)
|
||||
{
|
||||
@class.ClassName = codeDocument.Source.FilePath == null ? null : Path.GetFileNameWithoutExtension(codeDocument.Source.FilePath);
|
||||
}
|
||||
|
||||
if (@class.ClassName == null)
|
||||
{
|
||||
@class.ClassName = "__BlazorComponent";
|
||||
}
|
||||
|
||||
@class.ClassName = computedClass;
|
||||
@class.Modifiers.Clear();
|
||||
@class.Modifiers.Add("public");
|
||||
|
||||
|
|
@ -72,6 +90,50 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
method.Children.Insert(0, callBase);
|
||||
}
|
||||
|
||||
// In general documents will have a relative path (relative to the project root).
|
||||
// We can only really compute a nice class/namespace when we know a relative path.
|
||||
//
|
||||
// However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't
|
||||
// set up correctly.
|
||||
private bool TryComputeNamespaceAndClass(string filePath, string relativePath, out string @namespace, out string @class)
|
||||
{
|
||||
if (filePath == null || relativePath == null || filePath.Length <= relativePath.Length)
|
||||
{
|
||||
@namespace = null;
|
||||
@class = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try and infer a namespace from the project directory. We don't yet have the ability to pass
|
||||
// the namespace through from the project.
|
||||
var trimLength = relativePath.Length + (relativePath.StartsWith("/") ? 0 : 1);
|
||||
var baseDirectory = filePath.Substring(0, filePath.Length - trimLength);
|
||||
var baseNamespace = Path.GetFileName(baseDirectory);
|
||||
if (string.IsNullOrEmpty(baseNamespace))
|
||||
{
|
||||
@namespace = null;
|
||||
@class = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(baseNamespace); // Don't sanitize, we expect it to contain dots.
|
||||
|
||||
var segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Skip the last segment because it's the FileName.
|
||||
for (var i = 0; i < segments.Length - 1; i++)
|
||||
{
|
||||
builder.Append('.');
|
||||
builder.Append(CSharpIdentifier.SanitizeClassName(segments[i]));
|
||||
}
|
||||
|
||||
@namespace = builder.ToString();
|
||||
@class = CSharpIdentifier.SanitizeClassName(Path.GetFileNameWithoutExtension(relativePath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Workaround
|
||||
// This is a workaround for the fact that the base class doesn't provide good support
|
||||
// for replacing the IntermediateNodeWriter when building the code target.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class EliminateMethodBodyPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run early in the optimization phase
|
||||
public override int Order => Int32.MinValue;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
if (codeDocument == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(codeDocument));
|
||||
}
|
||||
|
||||
if (documentNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(documentNode));
|
||||
}
|
||||
|
||||
var method = documentNode.FindPrimaryMethod();
|
||||
method.Children.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,16 +28,5 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new ImplementsDirectivePass());
|
||||
}
|
||||
|
||||
public static void Register(IRazorEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new ImplementsDirectivePass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,12 +35,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
builder.Features.Add(new Pass());
|
||||
}
|
||||
|
||||
public static void Register(IRazorEngineBuilder builder)
|
||||
{
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new Pass());
|
||||
}
|
||||
|
||||
private class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
protected override void ExecuteCore(
|
||||
|
|
|
|||
|
|
@ -28,16 +28,5 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new LayoutDirectivePass());
|
||||
}
|
||||
|
||||
public static void Register(IRazorEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new LayoutDirectivePass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,5 +6,7 @@ using Microsoft.AspNetCore.Blazor.Razor;
|
|||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
[assembly: ProvideRazorExtensionInitializer("Blazor-0.1", typeof(BlazorExtensionInitializer))]
|
||||
[assembly: ProvideRazorExtensionInitializer("BlazorDeclaration-0.1", typeof(BlazorExtensionInitializer))]
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Test")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Razor.Extensions.Test")]
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
public class RazorCompilerDiagnostic
|
||||
{
|
||||
public DiagnosticType Type { get; }
|
||||
public string SourceFilePath { get; }
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
public string Message { get; }
|
||||
|
||||
public RazorCompilerDiagnostic(
|
||||
DiagnosticType type,
|
||||
string sourceFilePath,
|
||||
int line,
|
||||
int column,
|
||||
string message)
|
||||
{
|
||||
Type = type;
|
||||
SourceFilePath = sourceFilePath;
|
||||
Line = line;
|
||||
Column = column;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public enum DiagnosticType
|
||||
{
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public string FormatForConsole()
|
||||
=> $"{SourceFilePath}({Line},{Column}): {FormatTypeAndCodeForConsole()}: {Message}";
|
||||
|
||||
private string FormatTypeAndCodeForConsole()
|
||||
=> $"{Type.ToString().ToLowerInvariant()} Blazor";
|
||||
}
|
||||
}
|
||||
|
|
@ -12,25 +12,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
/// </summary>
|
||||
public class RazorCompilerException : Exception
|
||||
{
|
||||
private readonly int _line;
|
||||
private readonly int _column;
|
||||
|
||||
public RazorCompilerException(string message) : this(message, null)
|
||||
public RazorCompilerException(RazorDiagnostic diagnostic)
|
||||
{
|
||||
Diagnostic = diagnostic;
|
||||
}
|
||||
|
||||
public RazorCompilerException(string message, SourceSpan? source) : base(message)
|
||||
{
|
||||
_line = source.HasValue ? (source.Value.LineIndex + 1) : 1;
|
||||
_column = source.HasValue ? (source.Value.CharacterIndex + 1) : 1;
|
||||
}
|
||||
|
||||
public RazorCompilerDiagnostic ToDiagnostic(string sourceFilePath)
|
||||
=> new RazorCompilerDiagnostic(
|
||||
RazorCompilerDiagnostic.DiagnosticType.Error,
|
||||
sourceFilePath,
|
||||
line: _line,
|
||||
column: _column,
|
||||
message: Message);
|
||||
public RazorDiagnostic Diagnostic { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,15 +29,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
if (_stack.Count == 0)
|
||||
{
|
||||
throw new RazorCompilerException(
|
||||
$"Unexpected closing tag '{tagName}' with no matching start tag.", source);
|
||||
var diagnostic = BlazorDiagnosticFactory.Create_UnexpectedClosingTag(source ?? SourceSpan.Undefined, tagName);
|
||||
throw new RazorCompilerException(diagnostic);
|
||||
}
|
||||
|
||||
var currentScope = _stack.Pop();
|
||||
if (!tagName.Equals(currentScope.TagName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new RazorCompilerException(
|
||||
$"Mismatching closing tag. Found '{tagName}' but expected '{currentScope.TagName}'.", source);
|
||||
var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(source, currentScope.TagName, tagName);
|
||||
throw new RazorCompilerException(diagnostic);
|
||||
}
|
||||
|
||||
// Note: there's no unit test to cover the following, because there's no known way of
|
||||
|
|
@ -45,8 +45,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// just in case one day it turns out there is some way of causing this error.
|
||||
if (isComponent != currentScope.IsComponent)
|
||||
{
|
||||
throw new RazorCompilerException(
|
||||
$"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(currentScope.IsComponent ? "component" : "element")}'.", source);
|
||||
var kind = isComponent ? "component" : "element";
|
||||
var expectedKind = currentScope.IsComponent ? "component" : "element";
|
||||
var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTagKind(source, tagName, kind, expectedKind);
|
||||
throw new RazorCompilerException(diagnostic);
|
||||
}
|
||||
|
||||
// When closing the scope for a component with children, it's time to close the lambda
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
// 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 System.Reflection;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
public class ComponentRenderingRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void SupportsChildComponentsViaTemporarySyntax()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testComponentTypeName = FullTypeName<TestComponent>();
|
||||
var component = CompileToComponent($"<c:{testComponentTypeName} />");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 1, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPassParametersToComponents()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testComponentTypeName = FullTypeName<TestComponent>();
|
||||
var testObjectTypeName = FullTypeName<SomeType>();
|
||||
// TODO: Once we have the improved component tooling and can allow syntax
|
||||
// like StringProperty="My string" or BoolProperty=true, update this
|
||||
// test to use that syntax.
|
||||
var component = CompileToComponent($"<c:{testComponentTypeName}" +
|
||||
$" IntProperty=@(123)" +
|
||||
$" BoolProperty=@true" +
|
||||
$" StringProperty=@(\"My string\")" +
|
||||
$" ObjectProperty=@(new {testObjectTypeName}()) />");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 5, 0),
|
||||
frame => AssertFrame.Attribute(frame, "IntProperty", 123, 1),
|
||||
frame => AssertFrame.Attribute(frame, "BoolProperty", true, 2),
|
||||
frame => AssertFrame.Attribute(frame, "StringProperty", "My string", 3),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "ObjectProperty", 4);
|
||||
Assert.IsType<SomeType>(frame.AttributeValue);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIncludeChildrenInComponents()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testComponentTypeName = FullTypeName<TestComponent>();
|
||||
var component = CompileToComponent($"<c:{testComponentTypeName} MyAttr=@(\"abc\")>" +
|
||||
$"Some text" +
|
||||
$"<some-child a='1'>Nested text</some-child>" +
|
||||
$"</c:{testComponentTypeName}>");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert: component frames are correct
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "MyAttr", "abc", 1),
|
||||
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
|
||||
|
||||
// Assert: Captured ChildContent frames are correct
|
||||
var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue);
|
||||
Assert.Collection(childFrames,
|
||||
frame => AssertFrame.Text(frame, "Some text", 3),
|
||||
frame => AssertFrame.Element(frame, "some-child", 3, 4),
|
||||
frame => AssertFrame.Attribute(frame, "a", "1", 5),
|
||||
frame => AssertFrame.Text(frame, "Nested text", 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanNestComponentChildContent()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testComponentTypeName = FullTypeName<TestComponent>();
|
||||
var component = CompileToComponent(
|
||||
$"<c:{testComponentTypeName}>" +
|
||||
$"<c:{testComponentTypeName}>" +
|
||||
$"Some text" +
|
||||
$"</c:{testComponentTypeName}>" +
|
||||
$"</c:{testComponentTypeName}>");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert: outer component frames are correct
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1));
|
||||
|
||||
// Assert: first level of ChildContent is correct
|
||||
// Note that we don't really need the sequence numbers to continue on from the
|
||||
// sequence numbers at the parent level. All that really matters is that they are
|
||||
// correct relative to each other (i.e., incrementing) within the nesting level.
|
||||
// As an implementation detail, it happens that they do follow on from the parent
|
||||
// level, but we could change that part of the implementation if we wanted.
|
||||
var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray();
|
||||
Assert.Collection(innerFrames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 2, 2),
|
||||
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3));
|
||||
|
||||
// Assert: second level of ChildContent is correct
|
||||
Assert.Collection(GetFrames((RenderFragment)innerFrames[1].AttributeValue),
|
||||
frame => AssertFrame.Text(frame, "Some text", 4));
|
||||
}
|
||||
|
||||
public class SomeType { }
|
||||
|
||||
public class TestComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
// 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.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Razor;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
public class DeclarationRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
internal override RazorConfiguration Configuration => BlazorExtensionInitializer.DeclarationConfiguration;
|
||||
|
||||
[Fact]
|
||||
public void DeclarationConfiguration_IncludesFunctions()
|
||||
{
|
||||
// Arrange & Act
|
||||
var component = CompileToComponent(@"
|
||||
@functions {
|
||||
public string Value { get; set; }
|
||||
}");
|
||||
|
||||
// Assert
|
||||
var property = component.GetType().GetProperty("Value");
|
||||
Assert.NotNull(property);
|
||||
Assert.Same(typeof(string), property.PropertyType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclarationConfiguration_IncludesInject()
|
||||
{
|
||||
// Arrange & Act
|
||||
var component = CompileToComponent(@"
|
||||
@inject string Value
|
||||
");
|
||||
|
||||
// Assert
|
||||
var property = component.GetType().GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(property);
|
||||
Assert.Same(typeof(string), property.PropertyType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclarationConfiguration_IncludesUsings()
|
||||
{
|
||||
// Arrange & Act
|
||||
var component = CompileToComponent(@"
|
||||
@using System.Text
|
||||
@inject StringBuilder Value
|
||||
");
|
||||
|
||||
// Assert
|
||||
var property = component.GetType().GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(property);
|
||||
Assert.Same(typeof(StringBuilder), property.PropertyType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclarationConfiguration_IncludesInherits()
|
||||
{
|
||||
// Arrange & Act
|
||||
var component = CompileToComponent($@"
|
||||
@inherits {FullTypeName<BaseClass>()}
|
||||
");
|
||||
|
||||
// Assert
|
||||
Assert.Same(typeof(BaseClass), component.GetType().BaseType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclarationConfiguration_IncludesImplements()
|
||||
{
|
||||
// Arrange & Act
|
||||
var component = CompileToComponent($@"
|
||||
@implements {FullTypeName<IDoCoolThings>()}
|
||||
");
|
||||
|
||||
// Assert
|
||||
var type = component.GetType();
|
||||
Assert.Contains(typeof(IDoCoolThings), component.GetType().GetInterfaces());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclarationConfiguration_RenderMethodIsEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
var component = CompileToComponent(@"
|
||||
<html>
|
||||
@{ var message = ""hi""; }
|
||||
<span class=""@(5 + 7)"">@message</span>
|
||||
</html>
|
||||
");
|
||||
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(frames);
|
||||
}
|
||||
|
||||
public class BaseClass : BlazorComponent
|
||||
{
|
||||
}
|
||||
|
||||
public interface IDoCoolThings
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
public class DiagnosticRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void TemporaryComponentSyntaxRejectsParametersExpressedAsPlainHtmlAttributes()
|
||||
{
|
||||
// This is a temporary syntax restriction. Currently you can write:
|
||||
// <c:MyComponent MyParam=@("My value") />
|
||||
// ... but are *not* allowed to write:
|
||||
// <c:MyComponent MyParam="My value" />
|
||||
// This is because until we get the improved taghelper-based tooling,
|
||||
// we're using AngleSharp to parse the plain HTML attributes, and it
|
||||
// suffers from limitations:
|
||||
// * Loses the casing of attribute names (MyParam becomes myparam)
|
||||
// * Doesn't recognize MyBool=true as an bool (becomes mybool="true"),
|
||||
// plus equivalent for other primitives like enum values
|
||||
// So to avoid people getting runtime errors, we're currently imposing
|
||||
// the compile-time restriction that component params have to be given
|
||||
// as C# expressions, e.g., MyBool=@true and MyString=@("Hello")
|
||||
|
||||
// Arrange/Act
|
||||
var result = CompileToCSharp(
|
||||
$"Line 1\n" +
|
||||
$"Some text <c:MyComponent MyParam=\"My value\" />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result.Diagnostics,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("BL9980", item.Id);
|
||||
Assert.Equal(
|
||||
$"Wrong syntax for 'myparam' on 'c:MyComponent': As a temporary " +
|
||||
$"limitation, component attributes must be expressed with C# syntax. For " +
|
||||
$"example, SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" " +
|
||||
$"is not.", item.GetMessage());
|
||||
Assert.Equal(1, item.Span.LineIndex);
|
||||
Assert.Equal(10, item.Span.CharacterIndex);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsEndTagWithNoStartTag()
|
||||
{
|
||||
// Arrange/Act
|
||||
var result = CompileToCSharp(
|
||||
"Line1\nLine2\nLine3</mytag>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(result.Diagnostics,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("BL9981", item.Id);
|
||||
Assert.Equal("Unexpected closing tag 'mytag' with no matching start tag.", item.GetMessage());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsEndTagWithDifferentNameToStartTag()
|
||||
{
|
||||
// Arrange/Act
|
||||
var result = CompileToCSharp(
|
||||
$"@{{\n" +
|
||||
$" var abc = 123;\n" +
|
||||
$"}}\n" +
|
||||
$"<root>\n" +
|
||||
$" <other />\n" +
|
||||
$" text\n" +
|
||||
$" <child>more text</root>\n" +
|
||||
$"</child>\n");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(result.Diagnostics,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("BL9982", item.Id);
|
||||
Assert.Equal("Mismatching closing tag. Found 'child' but expected 'root'.", item.GetMessage());
|
||||
Assert.Equal(6, item.Span.LineIndex);
|
||||
Assert.Equal(20, item.Span.CharacterIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
// Integration tests for Blazor's directives
|
||||
public class DirectiveRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ComponentsDoNotHaveLayoutAttributeByDefault()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent($"Hello");
|
||||
|
||||
// Assert
|
||||
Assert.Null(component.GetType().GetCustomAttribute<LayoutAttribute>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsLayoutDeclarations()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testComponentTypeName = FullTypeName<TestLayout>();
|
||||
var component = CompileToComponent(
|
||||
$"@layout {testComponentTypeName}\n" +
|
||||
$"Hello");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
var layoutAttribute = component.GetType().GetCustomAttribute<LayoutAttribute>();
|
||||
Assert.NotNull(layoutAttribute);
|
||||
Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsImplementsDeclarations()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testInterfaceTypeName = FullTypeName<ITestInterface>();
|
||||
var component = CompileToComponent(
|
||||
$"@implements {testInterfaceTypeName}\n" +
|
||||
$"Hello");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.IsAssignableFrom<ITestInterface>(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsInheritsDirective()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testBaseClassTypeName = FullTypeName<TestBaseClass>();
|
||||
var component = CompileToComponent(
|
||||
$"@inherits {testBaseClassTypeName}" + Environment.NewLine +
|
||||
$"Hello");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.IsAssignableFrom<TestBaseClass>(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsInjectDirective()
|
||||
{
|
||||
// Arrange/Act 1: Compilation
|
||||
var componentType = CompileToComponent(
|
||||
$"@inject {FullTypeName<IMyService1>()} MyService1\n" +
|
||||
$"@inject {FullTypeName<IMyService2>()} MyService2\n" +
|
||||
$"Hello from @MyService1 and @MyService2").GetType();
|
||||
|
||||
// Assert 1: Compiled type has correct properties
|
||||
var propertyFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.NonPublic;
|
||||
var injectableProperties = componentType.GetProperties(propertyFlags)
|
||||
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null);
|
||||
Assert.Collection(injectableProperties.OrderBy(p => p.Name),
|
||||
property =>
|
||||
{
|
||||
Assert.Equal("MyService1", property.Name);
|
||||
Assert.Equal(typeof(IMyService1), property.PropertyType);
|
||||
Assert.False(property.GetMethod.IsPublic);
|
||||
Assert.False(property.SetMethod.IsPublic);
|
||||
},
|
||||
property =>
|
||||
{
|
||||
Assert.Equal("MyService2", property.Name);
|
||||
Assert.Equal(typeof(IMyService2), property.PropertyType);
|
||||
Assert.False(property.GetMethod.IsPublic);
|
||||
Assert.False(property.SetMethod.IsPublic);
|
||||
});
|
||||
|
||||
// Arrange/Act 2: DI-supplied component has correct behavior
|
||||
var serviceProvider = new TestServiceProvider();
|
||||
serviceProvider.AddService<IMyService1>(new MyService1Impl());
|
||||
serviceProvider.AddService<IMyService2>(new MyService2Impl());
|
||||
var componentFactory = new ComponentFactory(serviceProvider);
|
||||
var component = componentFactory.InstantiateComponent(componentType);
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert 2: Rendered component behaves correctly
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello from "),
|
||||
frame => AssertFrame.Text(frame, typeof(MyService1Impl).FullName),
|
||||
frame => AssertFrame.Text(frame, " and "),
|
||||
frame => AssertFrame.Text(frame, typeof(MyService2Impl).FullName));
|
||||
}
|
||||
|
||||
public class TestLayout : ILayoutComponent
|
||||
{
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface ITestInterface { }
|
||||
|
||||
public class TestBaseClass : BlazorComponent { }
|
||||
|
||||
public interface IMyService1 { }
|
||||
public interface IMyService2 { }
|
||||
public class MyService1Impl : IMyService1 { }
|
||||
public class MyService2Impl : IMyService2 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
// Integration tests focused on file path handling for class/namespace names
|
||||
public class FilePathRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void FileNameIsInvalidClasName_SanitizesInvalidClassName()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var result = CompileToAssembly("Filename with spaces.cshtml", "");
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result.Diagnostics);
|
||||
|
||||
var type = Assert.Single(result.Assembly.GetTypes());
|
||||
Assert.Equal(DefaultBaseNamespace, type.Namespace);
|
||||
Assert.Equal("Filename_with_spaces", type.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ItemAtRoot.cs", "Test", "ItemAtRoot")]
|
||||
[InlineData("Dir1\\MyFile.cs", "Test.Dir1", "MyFile")]
|
||||
[InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Dir1.Dir2", "MyFile")]
|
||||
public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName)
|
||||
{
|
||||
// Arrange
|
||||
relativePath = relativePath.Replace('\\', Path.DirectorySeparatorChar);
|
||||
|
||||
// Act
|
||||
var result = CompileToAssembly(relativePath, "");
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result.Diagnostics);
|
||||
|
||||
var type = Assert.Single(result.Assembly.GetTypes());
|
||||
Assert.Equal(expectedNamespace, type.Namespace);
|
||||
Assert.Equal(expectedClassName, type.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="RazorProjectItem"/> that does not exist.
|
||||
/// </summary>
|
||||
internal class NotFoundProjectItem : RazorProjectItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="NotFoundProjectItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="basePath">The base path.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
public NotFoundProjectItem(string basePath, string path)
|
||||
{
|
||||
BasePath = basePath;
|
||||
FilePath = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string BasePath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FilePath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Exists => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string PhysicalPath => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Stream Read() => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
// 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 = "Error";
|
||||
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 = "Error";
|
||||
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("Error");
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
internal class VirtualProjectItem : RazorProjectItem
|
||||
{
|
||||
private readonly byte[] _content;
|
||||
|
||||
public VirtualProjectItem(string basePath, string filePath, string physicalPath, string relativePhysicalPath, byte[] content)
|
||||
{
|
||||
BasePath = basePath;
|
||||
FilePath = filePath;
|
||||
PhysicalPath = physicalPath;
|
||||
RelativePhysicalPath = relativePhysicalPath;
|
||||
_content = content;
|
||||
}
|
||||
|
||||
public override string BasePath { get; }
|
||||
|
||||
public override string RelativePhysicalPath { get; }
|
||||
|
||||
public override string FilePath { get; }
|
||||
|
||||
public override string PhysicalPath { get; }
|
||||
|
||||
public override bool Exists => true;
|
||||
|
||||
public override Stream Read()
|
||||
{
|
||||
return new MemoryStream(_content);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,240 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Razor;
|
||||
using Microsoft.AspNetCore.Blazor.Rendering;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
public class RazorIntegrationTestBase
|
||||
{
|
||||
internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test";
|
||||
internal const string ArbitraryMacLinuxPath = "/dir/subdir/Test";
|
||||
|
||||
private RazorProjectEngine _projectEngine;
|
||||
|
||||
public RazorIntegrationTestBase()
|
||||
{
|
||||
Configuration = BlazorExtensionInitializer.DefaultConfiguration;
|
||||
FileSystem = new VirtualRazorProjectFileSystem();
|
||||
WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath;
|
||||
|
||||
DefaultBaseNamespace = "Test"; // Matches the default working directory
|
||||
DefaultFileName = "TestComponent.cshtml";
|
||||
}
|
||||
|
||||
internal virtual RazorConfiguration Configuration { get; }
|
||||
|
||||
internal virtual string DefaultBaseNamespace { get; }
|
||||
|
||||
internal virtual string DefaultFileName { get; }
|
||||
|
||||
internal virtual VirtualRazorProjectFileSystem FileSystem { get; }
|
||||
|
||||
internal virtual RazorProjectEngine ProjectEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_projectEngine == null)
|
||||
{
|
||||
_projectEngine = CreateProjectEngine();
|
||||
}
|
||||
|
||||
return _projectEngine;
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual string WorkingDirectory { get; }
|
||||
|
||||
internal RazorProjectEngine CreateProjectEngine()
|
||||
{
|
||||
return RazorProjectEngine.Create(Configuration, FileSystem, b =>
|
||||
{
|
||||
BlazorExtensionInitializer.Register(b);
|
||||
});
|
||||
}
|
||||
|
||||
protected CompileToCSharpResult CompileToCSharp(string cshtmlContent)
|
||||
{
|
||||
return CompileToCSharp(WorkingDirectory, DefaultFileName, cshtmlContent);
|
||||
}
|
||||
|
||||
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent)
|
||||
{
|
||||
return CompileToCSharp(WorkingDirectory, cshtmlRelativePath, cshtmlContent);
|
||||
}
|
||||
|
||||
protected CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent)
|
||||
{
|
||||
// FilePaths in Razor are **always** are of the form '/a/b/c.cshtml'
|
||||
var filePath = cshtmlRelativePath.Replace('\\', '/');
|
||||
if (!filePath.StartsWith('/'))
|
||||
{
|
||||
filePath = '/' + filePath;
|
||||
}
|
||||
|
||||
var projectItem = new VirtualProjectItem(
|
||||
cshtmlRootPath,
|
||||
filePath,
|
||||
Path.Combine(cshtmlRootPath, cshtmlRelativePath),
|
||||
cshtmlRelativePath,
|
||||
Encoding.UTF8.GetBytes(cshtmlContent));
|
||||
|
||||
var codeDocument = ProjectEngine.Process(projectItem);
|
||||
return new CompileToCSharpResult
|
||||
{
|
||||
CodeDocument = codeDocument,
|
||||
Code = codeDocument.GetCSharpDocument().GeneratedCode,
|
||||
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent)
|
||||
{
|
||||
return CompileToAssembly(WorkingDirectory, cshtmlRelativePath, cshtmlContent);
|
||||
}
|
||||
|
||||
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRootDirectory, string cshtmlRelativePath, string cshtmlContent)
|
||||
{
|
||||
var cSharpResult = CompileToCSharp(cshtmlRootDirectory, cshtmlRelativePath, cshtmlContent);
|
||||
return CompileToAssembly(cSharpResult);
|
||||
}
|
||||
|
||||
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult)
|
||||
{
|
||||
if (cSharpResult.Diagnostics.Any())
|
||||
{
|
||||
var diagnosticsLog = string.Join(Environment.NewLine, cSharpResult.Diagnostics.Select(d => d.ToString()).ToArray());
|
||||
throw new InvalidOperationException($"Aborting compilation to assembly because RazorCompiler returned nonempty diagnostics: {diagnosticsLog}");
|
||||
}
|
||||
|
||||
var syntaxTrees = new[]
|
||||
{
|
||||
CSharpSyntaxTree.ParseText(cSharpResult.Code)
|
||||
};
|
||||
var referenceAssembliesContainingTypes = new[]
|
||||
{
|
||||
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute), // System.Runtime
|
||||
typeof(BlazorComponent),
|
||||
typeof(RazorIntegrationTestBase), // Reference this assembly, so that we can refer to test component types
|
||||
};
|
||||
var references = referenceAssembliesContainingTypes
|
||||
.SelectMany(type => type.Assembly.GetReferencedAssemblies().Concat(new[] { type.Assembly.GetName() }))
|
||||
.Distinct()
|
||||
.Select(Assembly.Load)
|
||||
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
|
||||
.ToList();
|
||||
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
|
||||
var assemblyName = "TestAssembly" + Guid.NewGuid().ToString("N");
|
||||
var compilation = CSharpCompilation.Create(assemblyName,
|
||||
syntaxTrees,
|
||||
references,
|
||||
options);
|
||||
|
||||
using (var peStream = new MemoryStream())
|
||||
{
|
||||
compilation.Emit(peStream);
|
||||
|
||||
var diagnostics = compilation
|
||||
.GetDiagnostics()
|
||||
.Where(d => d.Severity != DiagnosticSeverity.Hidden);
|
||||
return new CompileToAssemblyResult
|
||||
{
|
||||
Diagnostics = diagnostics,
|
||||
Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected IComponent CompileToComponent(string cshtmlSource)
|
||||
{
|
||||
var assemblyResult = CompileToAssembly(WorkingDirectory, DefaultFileName, cshtmlSource);
|
||||
|
||||
var componentFullTypeName = $"{DefaultBaseNamespace}.{Path.GetFileNameWithoutExtension(DefaultFileName)}";
|
||||
return CompileToComponent(assemblyResult, componentFullTypeName);
|
||||
}
|
||||
|
||||
protected IComponent CompileToComponent(CompileToCSharpResult cSharpResult, string fullTypeName)
|
||||
{
|
||||
return CompileToComponent(CompileToAssembly(cSharpResult), fullTypeName);
|
||||
}
|
||||
|
||||
protected IComponent CompileToComponent(CompileToAssemblyResult assemblyResult, string fullTypeName)
|
||||
{
|
||||
Assert.Empty(assemblyResult.Diagnostics);
|
||||
|
||||
var componentType = assemblyResult.Assembly.GetType(fullTypeName);
|
||||
if (componentType == null)
|
||||
{
|
||||
throw new XunitException(
|
||||
$"Failed to find component type '{fullTypeName}'. Found types:" + Environment.NewLine +
|
||||
string.Join(Environment.NewLine, assemblyResult.Assembly.ExportedTypes.Select(t => t.FullName)));
|
||||
}
|
||||
|
||||
return (IComponent)Activator.CreateInstance(componentType);
|
||||
}
|
||||
|
||||
protected static string FullTypeName<T>() => typeof(T).FullName.Replace('+', '.');
|
||||
|
||||
protected RenderTreeFrame[] GetRenderTree(IComponent component)
|
||||
{
|
||||
var renderer = new TestRenderer();
|
||||
renderer.AttachComponent(component);
|
||||
component.SetParameters(ParameterCollection.Empty);
|
||||
return renderer.LatestBatchReferenceFrames;
|
||||
}
|
||||
|
||||
protected ArrayRange<RenderTreeFrame> GetFrames(RenderFragment fragment)
|
||||
{
|
||||
var builder = new RenderTreeBuilder(new TestRenderer());
|
||||
fragment(builder);
|
||||
return builder.GetFrames();
|
||||
}
|
||||
|
||||
protected class CompileToCSharpResult
|
||||
{
|
||||
public RazorCodeDocument CodeDocument { get; set; }
|
||||
public string Code { get; set; }
|
||||
public IEnumerable<RazorDiagnostic> Diagnostics { get; set; }
|
||||
}
|
||||
|
||||
protected class CompileToAssemblyResult
|
||||
{
|
||||
public Assembly Assembly { get; set; }
|
||||
public string VerboseLog { get; set; }
|
||||
public IEnumerable<Diagnostic> Diagnostics { get; set; }
|
||||
}
|
||||
|
||||
private class TestRenderer : Renderer
|
||||
{
|
||||
public TestRenderer() : base(new TestServiceProvider())
|
||||
{
|
||||
}
|
||||
|
||||
public RenderTreeFrame[] LatestBatchReferenceFrames { get; private set; }
|
||||
|
||||
public void AttachComponent(IComponent component)
|
||||
=> AssignComponentId(component);
|
||||
|
||||
protected override void UpdateDisplay(RenderBatch renderBatch)
|
||||
{
|
||||
LatestBatchReferenceFrames = renderBatch.ReferenceFrames.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
// Integration tests for the end-to-end of successful Razor compilation of component definitions
|
||||
// Includes running the component code to verify the output.
|
||||
public class RenderingRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void SupportsPlainText()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("Some plain text");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Some plain text", 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsCSharpExpressions()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(@"
|
||||
@(""Hello"")
|
||||
@((object)null)
|
||||
@(123)
|
||||
@(new object())
|
||||
");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Whitespace(frame, 0),
|
||||
frame => AssertFrame.Text(frame, "Hello", 1),
|
||||
frame => AssertFrame.Whitespace(frame, 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3), // @((object)null)
|
||||
frame => AssertFrame.Whitespace(frame, 4),
|
||||
frame => AssertFrame.Text(frame, "123", 5),
|
||||
frame => AssertFrame.Whitespace(frame, 6),
|
||||
frame => AssertFrame.Text(frame, new object().ToString(), 7),
|
||||
frame => AssertFrame.Whitespace(frame, 8));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsCSharpFunctionsBlock()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(@"
|
||||
@foreach(var item in items) {
|
||||
@item
|
||||
}
|
||||
@functions {
|
||||
string[] items = new[] { ""First"", ""Second"", ""Third"" };
|
||||
}
|
||||
");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Whitespace(frame, 0),
|
||||
frame => AssertFrame.Text(frame, "First", 1),
|
||||
frame => AssertFrame.Text(frame, "Second", 1),
|
||||
frame => AssertFrame.Text(frame, "Third", 1),
|
||||
frame => AssertFrame.Whitespace(frame, 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsElements()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("<myelem>Hello</myelem>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "myelem", 2, 0),
|
||||
frame => AssertFrame.Text(frame, "Hello", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsSelfClosingElements()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
|
||||
frame => AssertFrame.Element(frame, "myelem", 1, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsVoidHtmlElements()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("Some text so elem isn't at position 0 <img>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
|
||||
frame => AssertFrame.Element(frame, "img", 1, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsComments()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("Start<!-- My comment -->End");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Start", 0),
|
||||
frame => AssertFrame.Text(frame, "End", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithLiteralValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2' />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attrib-one", "Value 1", 1),
|
||||
frame => AssertFrame.Attribute(frame, "a2", "v2", 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithStringExpressionValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"@{ var myValue = \"My string\"; }"
|
||||
+ "<elem attr=@myValue />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attr", "My string", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithNonStringExpressionValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"@{ var myValue = 123; }"
|
||||
+ "<elem attr=@myValue />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attr", "123", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithInterpolatedStringExpressionValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"@{ var myValue = \"world\"; var myNum=123; }"
|
||||
+ "<elem attr=\"Hello, @myValue.ToUpperInvariant() with number @(myNum*2)!\" />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attr", "Hello, WORLD with number 246!", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithEventHandlerValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<elem attr=@MyHandleEvent />
|
||||
@functions {
|
||||
public bool HandlerWasCalled { get; set; } = false;
|
||||
|
||||
void MyHandleEvent(Microsoft.AspNetCore.Blazor.UIEventArgs eventArgs)
|
||||
{
|
||||
HandlerWasCalled = true;
|
||||
}
|
||||
}");
|
||||
var handlerWasCalledProperty = component.GetType().GetProperty("HandlerWasCalled");
|
||||
|
||||
// Assert
|
||||
Assert.False((bool)handlerWasCalledProperty.GetValue(component));
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame =>
|
||||
{
|
||||
Assert.Equal(RenderTreeFrameType.Attribute, frame.FrameType);
|
||||
Assert.Equal(1, frame.Sequence);
|
||||
Assert.NotNull(frame.AttributeValue);
|
||||
|
||||
((UIEventHandler)frame.AttributeValue)(null);
|
||||
Assert.True((bool)handlerWasCalledProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Whitespace(frame, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithCSharpCodeBlockValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<elem attr=@{ DidInvokeCode = true; } />
|
||||
@functions {
|
||||
public bool DidInvokeCode { get; set; } = false;
|
||||
}");
|
||||
var didInvokeCodeProperty = component.GetType().GetProperty("DidInvokeCode");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.False((bool)didInvokeCodeProperty.GetValue(component));
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame =>
|
||||
{
|
||||
Assert.Equal(RenderTreeFrameType.Attribute, frame.FrameType);
|
||||
Assert.NotNull(frame.AttributeValue);
|
||||
Assert.Equal(1, frame.Sequence);
|
||||
|
||||
((UIEventHandler)frame.AttributeValue)(null);
|
||||
Assert.True((bool)didInvokeCodeProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Whitespace(frame, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsUsingStatements()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"@using System.Collections.Generic
|
||||
@(typeof(List<string>).FullName)");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Whitespace(frame, 0),
|
||||
frame => AssertFrame.Text(frame, typeof(List<string>).FullName, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributeFramesEvaluatedInline()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<elem @onclick(MyHandler) />
|
||||
@functions {
|
||||
public bool DidInvokeCode { get; set; } = false;
|
||||
void MyHandler()
|
||||
{
|
||||
DidInvokeCode = true;
|
||||
}
|
||||
}");
|
||||
var didInvokeCodeProperty = component.GetType().GetProperty("DidInvokeCode");
|
||||
|
||||
// Assert
|
||||
Assert.False((bool)didInvokeCodeProperty.GetValue(component));
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame =>
|
||||
{
|
||||
Assert.Equal(RenderTreeFrameType.Attribute, frame.FrameType);
|
||||
Assert.NotNull(frame.AttributeValue);
|
||||
Assert.Equal(1, frame.Sequence);
|
||||
|
||||
((UIEventHandler)frame.AttributeValue)(null);
|
||||
Assert.True((bool)didInvokeCodeProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Whitespace(frame, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTwoWayBindingForTextboxes()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyValue) />
|
||||
@functions {
|
||||
public string MyValue { get; set; } = ""Initial value"";
|
||||
}");
|
||||
var myValueProperty = component.GetType().GetProperty("MyValue");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "input", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", "Initial value", 1),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onchange", 2);
|
||||
|
||||
// Trigger the change event to show it updates the property
|
||||
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
|
||||
{
|
||||
Value = "Modified value"
|
||||
});
|
||||
Assert.Equal("Modified value", myValueProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Text(frame, "\n", 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTwoWayBindingForDateValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyDate) />
|
||||
@functions {
|
||||
public DateTime MyDate { get; set; } = new DateTime(2018, 3, 4, 1, 2, 3);
|
||||
}");
|
||||
var myDateProperty = component.GetType().GetProperty("MyDate");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "input", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", new DateTime(2018, 3, 4, 1, 2, 3).ToString(), 1),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onchange", 2);
|
||||
|
||||
// Trigger the change event to show it updates the property
|
||||
var newDateValue = new DateTime(2018, 3, 5, 4, 5, 6);
|
||||
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
|
||||
{
|
||||
Value = newDateValue.ToString()
|
||||
});
|
||||
Assert.Equal(newDateValue, myDateProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Text(frame, "\n", 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTwoWayBindingForDateValuesWithFormatString()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyDate, ""ddd yyyy-MM-dd"") />
|
||||
@functions {
|
||||
public DateTime MyDate { get; set; } = new DateTime(2018, 3, 4);
|
||||
}");
|
||||
var myDateProperty = component.GetType().GetProperty("MyDate");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "input", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", "Sun 2018-03-04", 1),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onchange", 2);
|
||||
|
||||
// Trigger the change event to show it updates the property
|
||||
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
|
||||
{
|
||||
Value = "Mon 2018-03-05"
|
||||
});
|
||||
Assert.Equal(new DateTime(2018, 3, 5), myDateProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Text(frame, "\n", 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTwoWayBindingForBoolValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyValue) />
|
||||
@functions {
|
||||
public bool MyValue { get; set; } = true;
|
||||
}");
|
||||
var myValueProperty = component.GetType().GetProperty("MyValue");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "input", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", "True", 1),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onchange", 2);
|
||||
|
||||
// Trigger the change event to show it updates the property
|
||||
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
|
||||
{
|
||||
Value = false
|
||||
});
|
||||
Assert.False((bool)myValueProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Text(frame, "\n", 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTwoWayBindingForEnumValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var myEnumType = FullTypeName<MyEnum>();
|
||||
var component = CompileToComponent(
|
||||
$@"<input @bind(MyValue) />
|
||||
@functions {{
|
||||
public {myEnumType} MyValue {{ get; set; }} = {myEnumType}.{nameof(MyEnum.FirstValue)};
|
||||
}}");
|
||||
var myValueProperty = component.GetType().GetProperty("MyValue");
|
||||
|
||||
// Assert
|
||||
var frames = GetRenderTree(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "input", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", MyEnum.FirstValue.ToString(), 1),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onchange", 2);
|
||||
|
||||
// Trigger the change event to show it updates the property
|
||||
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
|
||||
{
|
||||
Value = MyEnum.SecondValue.ToString()
|
||||
});
|
||||
Assert.Equal(MyEnum.SecondValue, (MyEnum)myValueProperty.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Text(frame, "\n", 3));
|
||||
}
|
||||
|
||||
public enum MyEnum { FirstValue, SecondValue }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Razor;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
|
@ -17,6 +18,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Blazor
|
|||
{
|
||||
configure?.Invoke(b);
|
||||
new BlazorExtensionInitializer().Initialize(b);
|
||||
|
||||
var classifier = b.Features.OfType<ComponentDocumentClassifierPass>().Single();
|
||||
classifier.MangleClassNames = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue