From 57a04fb178ad31a67b01595df5f65c84c9f9c766 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 19 Feb 2018 16:16:44 -0800 Subject: [PATCH] Adopt more of Razor Exensibility Removes some workarounds and uses Razor extensibility in a few more places. _ViewImports now works in VS --- .../Core/RazorCompilation/RazorCompiler.cs | 4 +- .../Microsoft.AspNetCore.Blazor.Build.csproj | 8 ++ .../BlazorApi.cs | 52 ++++++++++ .../BlazorCodeTarget.cs | 46 ++++++++- .../BlazorComponent.cs | 12 --- .../BlazorConfiguration.cs | 16 ++++ .../BlazorExtensionInitializer.cs | 83 ++++++++++++++++ .../BlazorImportProjectFeature.cs | 79 ++++++++++++++++ .../BlazorIntermediateNodeWriter.cs | 20 ++-- .../BlazorLoweringPhase.cs | 75 --------------- .../BlazorRazorEngine.cs | 46 --------- .../BlazorTemplateEngine.cs | 20 +++- .../ComponentDocumentClassifierPass.cs | 94 +++++++++++++++++++ .../InjectDirective.cs | 6 ++ ....AspNetCore.Blazor.Razor.Extensions.csproj | 3 + .../Properties/AssemblyInfo.cs | 7 ++ .../RenderTreeBuilder.cs | 32 ------- .../ScopeStack.cs | 6 +- .../Temporary/TemporaryImplementsPass.cs | 9 +- .../Temporary/TemporaryLayoutPass.cs | 9 +- .../Components/BlazorComponent.cs | 2 +- .../RenderTree/RenderTreeBuilder.cs | 2 +- .../BlazorProjectEngineFactory.cs | 23 +++++ 23 files changed, 461 insertions(+), 193 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorComponent.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorConfiguration.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorImportProjectFeature.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorLoweringPhase.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs create mode 100644 tooling/Microsoft.VisualStudio.LanguageServices.Blazor/BlazorProjectEngineFactory.cs diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs index ce3d4cb726..1fdb181f40 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs @@ -93,9 +93,9 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation // name and any public members. Don't need to actually emit all the RenderTreeBuilder // invocations. - var engine = new BlazorRazorEngine(); + var engine = RazorEngine.Create(b => BlazorExtensionInitializer.Register(b)); var blazorTemplateEngine = new BlazorTemplateEngine( - engine.Engine, + engine, RazorProjectFileSystem.Create(inputRootPath)); var codeDoc = blazorTemplateEngine.CreateCodeDocument( new BlazorProjectItem(inputRootPath, inputFilePath, inputFileContents)); diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj index ecdf6366d3..acaa68d5eb 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj @@ -31,6 +31,14 @@ + + + + PreserveNewest + false + + + diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs new file mode 100644 index 0000000000..ae9cfa0e47 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs @@ -0,0 +1,52 @@ +// 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 +{ + // Constants for method names used in code-generation + // Keep these in sync with the actual definitions + internal static class BlazorApi + { + public static class BlazorComponent + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.BlazorComponent"; + + public static readonly string BuildRenderTree = nameof(BuildRenderTree); + } + + public static class RenderFragment + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.RenderFragment"; + } + + public static class RenderTreeBuilder + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder"; + + public static readonly string OpenElement = nameof(OpenElement); + + public static readonly string CloseElement = nameof(CloseElement); + + public static readonly string OpenComponent = nameof(OpenComponent); + + public static readonly string CloseComponent = nameof(CloseElement); + + public static readonly string AddContent = nameof(AddContent); + + public static readonly string AddAttribute = nameof(AddAttribute); + + public static readonly string Clear = nameof(Clear); + + public static readonly string GetFrames = nameof(GetFrames); + + public static readonly string ChildContent = nameof(ChildContent); + } + + public static class BindMethods + { + public static readonly string GetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.GetValue"; + + public static readonly string SetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValue"; + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorCodeTarget.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorCodeTarget.cs index c2f2557d6d..13d3e3ed41 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorCodeTarget.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorCodeTarget.cs @@ -1,7 +1,9 @@ // 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 Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; namespace Microsoft.AspNetCore.Blazor.Razor @@ -11,11 +13,47 @@ namespace Microsoft.AspNetCore.Blazor.Razor /// internal class BlazorCodeTarget : CodeTarget { + private readonly RazorCodeGenerationOptions _options; + + public BlazorCodeTarget(RazorCodeGenerationOptions options, IEnumerable extensions) + { + _options = options; + Extensions = extensions.ToArray(); + } + + public ICodeTargetExtension[] Extensions { get; } + public override IntermediateNodeWriter CreateNodeWriter() - => new BlazorIntermediateNodeWriter(); + { + return _options.DesignTime ? (IntermediateNodeWriter)new DesignTimeNodeWriter() : new BlazorIntermediateNodeWriter(); + } - public override TExtension GetExtension() => null; + public override TExtension GetExtension() + { + for (var i = 0; i < Extensions.Length; i++) + { + var match = Extensions[i] as TExtension; + if (match != null) + { + return match; + } + } - public override bool HasExtension() => false; + return null; + } + + public override bool HasExtension() + { + for (var i = 0; i < Extensions.Length; i++) + { + var match = Extensions[i] as TExtension; + if (match != null) + { + return true; + } + } + + return false; + } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorComponent.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorComponent.cs deleted file mode 100644 index 634555a5e1..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorComponent.cs +++ /dev/null @@ -1,12 +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 -{ - internal static class BlazorComponent - { - public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.BlazorComponent"; - - public static readonly string BuildRenderTree = nameof(BuildRenderTree); - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorConfiguration.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorConfiguration.cs new file mode 100644 index 0000000000..4fcab4fcd0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorConfiguration.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + public static class BlazorConfiguration + { + public static readonly RazorConfiguration Default = new RazorConfiguration( + RazorLanguageVersion.Version_2_1, + "Blazor-0.1", + Array.Empty()); + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs new file mode 100644 index 0000000000..8d0ba17784 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs @@ -0,0 +1,83 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + public class BlazorExtensionInitializer : RazorExtensionInitializer + { + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + FunctionsDirective.Register(builder); + InjectDirective.Register(builder); + InheritsDirective.Register(builder); + TemporaryLayoutPass.Register(builder); + TemporaryImplementsPass.Register(builder); + + builder.Features.Remove(builder.Features.OfType().Single()); + builder.Features.Add(new BlazorImportProjectFeature()); + + 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) + { + throw new ArgumentNullException(nameof(builder)); + } + + FunctionsDirective.Register(builder); + InjectDirective.Register(builder); + InheritsDirective.Register(builder); + TemporaryLayoutPass.Register(builder); + TemporaryImplementsPass.Register(builder); + + builder.Features.Add(new ConfigureBlazorCodeGenerationOptions()); + + builder.Features.Add(new ComponentDocumentClassifierPass()); + } + + public override void Initialize(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + Register(builder); + } + + private class ConfigureBlazorCodeGenerationOptions : IConfigureRazorCodeGenerationOptionsFeature + { + public int Order => 0; + + public RazorEngine Engine { get; set; } + + public void Configure(RazorCodeGenerationOptionsBuilder options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // These metadata attributes require a reference to the Razor.Runtime package which we don't + // otherwise need. + options.SuppressMetadataAttributes = true; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorImportProjectFeature.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorImportProjectFeature.cs new file mode 100644 index 0000000000..d79794be42 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorImportProjectFeature.cs @@ -0,0 +1,79 @@ +// 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.Text; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + internal class BlazorImportProjectFeature : IImportProjectFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + public RazorProjectItem DefaultImports => VirtualProjectItem.Instance; + + public RazorProjectEngine ProjectEngine { get; set; } + + public IReadOnlyList GetImports(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var imports = new List() + { + VirtualProjectItem.Instance, + }; + + // We add hierarchical imports second so any default directive imports can be overridden. + imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem)); + + return imports; + } + + // Temporary API until we fully convert to RazorProjectEngine + public IEnumerable GetHierarchicalImports(RazorProject project, RazorProjectItem projectItem) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + return project.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); + } + + private class VirtualProjectItem : RazorProjectItem + { + private readonly byte[] _defaultImportBytes; + + private VirtualProjectItem() + { + var preamble = Encoding.UTF8.GetPreamble(); + var content = @" +@using System +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + _defaultImportBytes = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(_defaultImportBytes, 0); + contentBytes.CopyTo(_defaultImportBytes, preamble.Length); + } + + public override string BasePath => null; + + public override string FilePath => null; + + public override string PhysicalPath => null; + + public override bool Exists => true; + + public static VirtualProjectItem Instance { get; } = new VirtualProjectItem(); + + public override Stream Read() => new MemoryStream(_defaultImportBytes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs index 5f921bcfbe..5a6d7eb687 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs @@ -126,7 +126,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor // text to display _scopeStack.IncrementCurrentScopeChildCount(context); context.CodeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddContent)}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator(); @@ -216,7 +216,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor // Text node _scopeStack.IncrementCurrentScopeChildCount(context); codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddContent)}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(nextToken.Data) @@ -237,14 +237,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor if (isComponent) { codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.OpenComponent)}<{componentTypeName}>") .Write((_sourceSequence++).ToString()) .WriteEndMethodInvocation(); } else { codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenElement)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.OpenElement)}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(nextTag.Data) @@ -294,8 +294,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor isComponent: isComponent, source: CalculateSourcePosition(node.Source, nextToken.Position)); var closeMethodName = isComponent - ? nameof(RenderTreeBuilder.CloseComponent) - : nameof(RenderTreeBuilder.CloseElement); + ? nameof(BlazorApi.RenderTreeBuilder.CloseComponent) + : nameof(BlazorApi.RenderTreeBuilder.CloseElement); codeWriter .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{closeMethodName}") .WriteEndMethodInvocation(); @@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor WriteAttribute(context.CodeWriter, "value", new IntermediateToken { Kind = TokenKind.CSharp, - Content = $"{RenderTreeBuilder.BindMethodsGetValue}({valueParams})" + Content = $"{BlazorApi.BindMethods.GetValue}({valueParams})" }); // [2] @onchange(BindSetValue(parsed => { X = parsed; }, X, Y, Z, ...)) @@ -350,7 +350,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor AttributeValue = new IntermediateToken { Kind = TokenKind.CSharp, - Content = $"onchange({RenderTreeBuilder.BindMethodsSetValue}({parsedArgsJoined}))" + Content = $"onchange({BlazorApi.BindMethods.SetValue}({parsedArgsJoined}))" } }; WriteElementAttributeToken(context, tag, onChangeAttributeToken); @@ -360,7 +360,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor // For any other attribute token (e.g., @onclick(...)), treat it as an expression // that will evaluate as an attribute frame context.CodeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .Write(token.AttributeValue.Content) @@ -439,7 +439,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor public void BeginWriteAttribute(CodeWriter codeWriter, string key) { codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}") .Write((_sourceSequence++).ToString()) .WriteParameterSeparator() .WriteStringLiteral(key) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorLoweringPhase.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorLoweringPhase.cs deleted file mode 100644 index be2583a099..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorLoweringPhase.cs +++ /dev/null @@ -1,75 +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 Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.CodeGeneration; -using Microsoft.AspNetCore.Razor.Language.Intermediate; - -namespace Microsoft.AspNetCore.Blazor.Razor -{ - /// - /// A phase that builds the C# document corresponding to - /// a for a Blazor component. - /// - internal class BlazorLoweringPhase : IRazorCSharpLoweringPhase - { - private readonly RazorCodeGenerationOptions _codegenOptions; - - public BlazorLoweringPhase(RazorCodeGenerationOptions codegenOptions) - { - _codegenOptions = codegenOptions - ?? throw new ArgumentNullException(nameof(codegenOptions)); - } - - public RazorEngine Engine { get; set; } - - public void Execute(RazorCodeDocument codeDocument) - { - var writer = BlazorComponentDocumentWriter.Create(_codegenOptions); - var documentNode = codeDocument.GetDocumentIntermediateNode(); - ConvertToBlazorPrimaryMethod(documentNode); - var csharpDoc = writer.WriteDocument(codeDocument, documentNode); - codeDocument.SetCSharpDocument(csharpDoc); - } - - private void ConvertToBlazorPrimaryMethod(DocumentIntermediateNode documentNode) - { - // Replaces the default "ExecuteAsync" method with Blazor's "BuildRenderTree". - // Note that DefaultDocumentWriter's VisitMethodDeclaration is hardcoded to - // emit methods with no parameters, so there's no way of setting the parameters - // from here. We inject the parameter later in RazorCompiler. - var primaryMethod = documentNode.FindPrimaryMethod(); - primaryMethod.ReturnType = "void"; - primaryMethod.MethodName = BlazorComponent.BuildRenderTree; - primaryMethod.Modifiers.Clear(); - primaryMethod.Modifiers.Add("protected"); - primaryMethod.Modifiers.Add("override"); - - var line = new CSharpCodeIntermediateNode(); - line.Children.Add(new IntermediateToken - { - Kind = TokenKind.CSharp, - Content = $"base.{primaryMethod.MethodName}(builder);" + Environment.NewLine - }); - primaryMethod.Children.Insert(0, line); - } - - /// - /// Creates instances that are configured to use - /// . - /// - private class BlazorComponentDocumentWriter : DocumentWriter - { - public static DocumentWriter Create(RazorCodeGenerationOptions options) - => Instance.Create(new BlazorCodeTarget(), options); - - private static BlazorComponentDocumentWriter Instance - = new BlazorComponentDocumentWriter(); - - public override RazorCSharpDocument WriteDocument( - RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) - => throw new NotImplementedException(); - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs deleted file mode 100644 index fab6f1136d..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs +++ /dev/null @@ -1,46 +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.Linq; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Extensions; - -namespace Microsoft.AspNetCore.Blazor.Razor -{ - /// - /// Wraps , configuring it to compile Blazor components. - /// - public class BlazorRazorEngine - { - private readonly RazorEngine _engine; - private readonly RazorCodeGenerationOptions _codegenOptions; - - public RazorEngine Engine => _engine; - - public BlazorRazorEngine() - { - _codegenOptions = RazorCodeGenerationOptions.CreateDefault(); - - _engine = RazorEngine.Create(configure => - { - FunctionsDirective.Register(configure); - InheritsDirective.Register(configure); - InjectDirective.Register(configure); - TemporaryLayoutPass.Register(configure); - TemporaryImplementsPass.Register(configure); - - configure.SetBaseType(BlazorComponent.FullTypeName); - - configure.Phases.Remove( - configure.Phases.OfType().Single()); - configure.Phases.Add(new BlazorLoweringPhase(_codegenOptions)); - - configure.ConfigureClass((codeDoc, classNode) => - { - configure.SetNamespace((string)codeDoc.Items[BlazorCodeDocItems.Namespace]); - classNode.ClassName = (string)codeDoc.Items[BlazorCodeDocItems.ClassName]; - }); - }); - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorTemplateEngine.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorTemplateEngine.cs index abba4ececf..da0450ec10 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorTemplateEngine.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorTemplateEngine.cs @@ -4,6 +4,7 @@ using System.IO; using Microsoft.AspNetCore.Razor.Language; using System.Text; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Blazor.Razor { @@ -12,11 +13,26 @@ namespace Microsoft.AspNetCore.Blazor.Razor /// 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) { - Options.ImportsFileName = "_ViewImports.cshtml"; - Options.DefaultImports = GetDefaultImports(); + _feature = new BlazorImportProjectFeature(); + + Options.DefaultImports = RazorSourceDocument.ReadFrom(_feature.DefaultImports); + } + + public override IEnumerable GetImportItems(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new System.ArgumentNullException(nameof(projectItem)); + } + + return _feature.GetHierarchicalImports(Project, projectItem); } private static RazorSourceDocument GetDefaultImports() diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs new file mode 100644 index 0000000000..3307af5d87 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs @@ -0,0 +1,94 @@ +// 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; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + internal class ComponentDocumentClassifierPass : DocumentClassifierPassBase, IRazorDocumentClassifierPass + { + protected override string DocumentKind => "Blazor.Component-0.1"; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + // Treat everything as a component by default if Blazor is part of the configuration. + return true; + } + + protected override void OnDocumentStructureCreated( + RazorCodeDocument codeDocument, + NamespaceDeclarationIntermediateNode @namespace, + ClassDeclarationIntermediateNode @class, + MethodDeclarationIntermediateNode method) + { + @namespace.Content = (string)codeDocument.Items[BlazorCodeDocItems.Namespace]; + if (@namespace.Content == null) + { + @namespace.Content = "Blazor"; + } + + @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.Modifiers.Clear(); + @class.Modifiers.Add("public"); + + method.ReturnType = "void"; + method.MethodName = BlazorApi.BlazorComponent.BuildRenderTree; + method.Modifiers.Clear(); + method.Modifiers.Add("protected"); + method.Modifiers.Add("override"); + + method.Parameters.Clear(); + method.Parameters.Add(new MethodParameter() + { + ParameterName = "builder", + TypeName = BlazorApi.RenderTreeBuilder.FullTypeName, + }); + + // We need to call the 'base' method as the first statement. + var callBase = new CSharpCodeIntermediateNode(); + callBase.Children.Add(new IntermediateToken + { + Kind = TokenKind.CSharp, + Content = $"base.{BlazorApi.BlazorComponent.BuildRenderTree}(builder);" + Environment.NewLine + }); + method.Children.Insert(0, callBase); + } + + #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. + void IRazorDocumentClassifierPass.Execute(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + base.Execute(codeDocument, documentNode); + documentNode.Target = new BlazorCodeTarget(documentNode.Options, _targetExtensions); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + + var feature = Engine.Features.OfType(); + _targetExtensions = feature.FirstOrDefault()?.TargetExtensions.ToArray() ?? EmptyExtensionArray; + } + + private static readonly ICodeTargetExtension[] EmptyExtensionArray = new ICodeTargetExtension[0]; + private ICodeTargetExtension[] _targetExtensions; + #endregion + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/InjectDirective.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/InjectDirective.cs index 610b85c88c..9d8b5dab4a 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/InjectDirective.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/InjectDirective.cs @@ -29,6 +29,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor builder.Description = "Inject a service from the application's service container into a property."; }); + public static void Register(RazorProjectEngineBuilder builder) + { + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + } + public static void Register(IRazorEngineBuilder builder) { builder.AddDirective(Directive); diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj index afcdc8d1dd..ded47d31b5 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj @@ -4,6 +4,9 @@ netstandard2.0 $(TargetFrameworks);net461 Microsoft.AspNetCore.Blazor.Razor + + + true diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4b642e6ea5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Blazor.Razor; +using Microsoft.AspNetCore.Razor.Language; + +[assembly: ProvideRazorExtensionInitializer("Blazor-0.1", typeof(BlazorExtensionInitializer))] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs deleted file mode 100644 index 398c5ad064..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs +++ /dev/null @@ -1,32 +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 -{ - // Constants for method names used in code-generation - // Keep these in sync with the actual RenderTreeBuilder definitions - internal static class RenderTreeBuilder - { - public static readonly string OpenElement = nameof(OpenElement); - - public static readonly string CloseElement = nameof(CloseElement); - - public static readonly string OpenComponent = nameof(OpenComponent); - - public static readonly string CloseComponent = nameof(CloseElement); - - public static readonly string AddContent = nameof(AddContent); - - public static readonly string AddAttribute = nameof(AddAttribute); - - public static readonly string Clear = nameof(Clear); - - public static readonly string GetFrames = nameof(GetFrames); - - public static readonly string ChildContent = nameof(ChildContent); - - public static readonly string BindMethodsGetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.GetValue"; - - public static readonly string BindMethodsSetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValue"; - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs index 4b8ce02301..79895409e1 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs @@ -15,8 +15,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor /// internal class ScopeStack { - private const string _renderFragmentTypeName = "Microsoft.AspNetCore.Blazor.RenderFragment"; - private readonly Stack _stack = new Stack(); private int _builderVarNumber = 1; @@ -72,9 +70,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor // When we're about to insert the first child into a component, // it's time to open a new lambda var blazorNodeWriter = (BlazorIntermediateNodeWriter)context.NodeWriter; - blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, RenderTreeBuilder.ChildContent); + blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, BlazorApi.RenderTreeBuilder.ChildContent); OffsetBuilderVarNumber(1); - context.CodeWriter.Write($"({_renderFragmentTypeName})("); + context.CodeWriter.Write($"({BlazorApi.RenderFragment.FullTypeName})("); currentScope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName); } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs index 8b1e50fb19..731961ef75 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs @@ -22,9 +22,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor // Captures: MyApp.Namespace.ISomeType private const string ImplementsTokenPattern = @"\s*Implements\s*<(.+)\>\s*\(\s*\)\s*"; - public static void Register(IRazorEngineBuilder configuration) + public static void Register(IRazorEngineBuilder builder) { - configuration.Features.Add(new TemporaryImplementsPass()); + builder.Features.Add(new TemporaryImplementsPass()); + } + + public static void Register(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new TemporaryImplementsPass()); } private TemporaryImplementsPass() : base(ImplementsTokenPattern) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs index bfc7f89c09..69ec91f3ed 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs @@ -25,9 +25,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor private const string LayoutAttributeTypeName = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute"; - public static void Register(IRazorEngineBuilder configuration) + public static void Register(IRazorEngineBuilder builder) { - configuration.Features.Add(new TemporaryLayoutPass()); + builder.Features.Add(new TemporaryLayoutPass()); + } + + public static void Register(RazorProjectEngineBuilder builder) + { + builder.Features.Add(new TemporaryLayoutPass()); } private TemporaryLayoutPass() : base(LayoutTokenPattern) diff --git a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs index c4b7989e14..1f947a25ed 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Blazor.Components // IMPORTANT // // Many of these names are used in code generation. Keep these in sync with the code generation code - // See: src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorComponent.cs + // See: src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs // Most of the developer-facing component lifecycle concepts are encapsulated in this // base class. The core Blazor rendering system doesn't know about them (it only knows diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs index 4b40e9fe38..698f6ce51c 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // IMPORTANT // // Many of these names are used in code generation. Keep these in sync with the code generation code - // See: src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs + // See: src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs /// /// Provides methods for building a collection of entries. diff --git a/tooling/Microsoft.VisualStudio.LanguageServices.Blazor/BlazorProjectEngineFactory.cs b/tooling/Microsoft.VisualStudio.LanguageServices.Blazor/BlazorProjectEngineFactory.cs new file mode 100644 index 0000000000..2b283624db --- /dev/null +++ b/tooling/Microsoft.VisualStudio.LanguageServices.Blazor/BlazorProjectEngineFactory.cs @@ -0,0 +1,23 @@ +// 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.Blazor.Razor; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.LanguageServices.Blazor +{ + [ExportCustomProjectEngineFactory("Blazor-0.1", SupportsSerialization = false)] + internal class BlazorProjectEngineFactory : IProjectEngineFactory + { + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + configure?.Invoke(b); + new BlazorExtensionInitializer().Initialize(b); + }); + } + } +}