Implement 2-phase compilation

This is a working (all tests passing) implementation of the two-phase
compilation system we will need for component discovery.

This builds on top of work we've doing in Razor, including the Razor
SDK, MSBuild tasks, and CLI/server.

This currently *does* discovery components during the build process, but
it doesn't use that data for anything yet.

It works like this:
1. Generate class declarations (structure only, no method bodies)
2. Compile a 'temp' assembly using the .cs files and output of 1.
3. Do component discovery using the 'temp' assembly
4. Generate class definitions (including method bodies)
5. Compile the 'real' assembly using the .cs files and output of 4.
This commit is contained in:
Ryan Nowak 2018-02-25 19:17:21 -08:00 committed by Steve Sanderson
parent 6182e8448d
commit 211561a6a6
11 changed files with 603 additions and 159 deletions

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Razor/2.1.0-preview2-30159">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<!-- Local alternative to <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" /> -->

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Razor/2.1.0-preview2-30159">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Razor/2.1.0-preview2-30159;Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

View File

@ -1,96 +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 Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
{
internal class BuildRazorCommand
{
public static void Command(CommandLineApplication command)
{
// Later, we might want to have the complete list of inputs passed in from MSBuild
// so developers can include/exclude whatever they want. The MVC Razor view precompiler
// does this by writing the list to a temporary 'response' file then passing the path
// to that file into its build executable (see: https://github.com/aspnet/MvcPrecompilation/blob/dev/src/Microsoft.AspNetCore.Mvc.Razor.ViewCompilation/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.ViewCompilation.targets)
// For now it's sufficient to assume we want to include '<sourcedir>**\*.cshtml'
var sourceDirPath = command.Option("--source",
"The path to the directory containing Razor files",
CommandOptionType.SingleValue);
var outputFilePath = command.Option("--output",
"The location where the resulting C# source file should be written",
CommandOptionType.SingleValue);
var baseNamespace = command.Option("--namespace",
"The base namespace for the generated C# classes.",
CommandOptionType.SingleValue);
var verboseFlag = command.Option("--verbose",
"Indicates that verbose console output should written",
CommandOptionType.NoValue);
command.OnExecute(() =>
{
if (!VerifyRequiredOptionsProvided(sourceDirPath, outputFilePath, baseNamespace))
{
return 1;
}
var sourceDirPathValue = sourceDirPath.Value();
if (!Directory.Exists(sourceDirPathValue))
{
Console.WriteLine($"ERROR: Directory not found: {sourceDirPathValue}");
return 1;
}
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()))
{
foreach (var sourceFile in sourceFiles)
{
var item = fileSystem.GetItem(sourceFile);
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;
});
}
private static IEnumerable<string> FindRazorFiles(string rootDirPath)
=> Directory.GetFiles(rootDirPath, "*.cshtml", SearchOption.AllDirectories);
private static bool VerifyRequiredOptionsProvided(params CommandOption[] options)
{
var violations = options.Where(o => !o.HasValue()).ToList();
foreach (var violation in violations)
{
Console.WriteLine($"ERROR: No value specified for required option '{violation.LongName}'.");
}
return !violations.Any();
}
}
}

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Blazor.Build
app.HelpOption("-?|-h|--help");
app.Command("build", BuildCommand.Command);
app.Command("buildrazor", BuildRazorCommand.Command);
if (args.Length > 0)
{

View File

@ -10,6 +10,10 @@
<Import Project="$(MSBuildThisFileDirectory)targets\All.targets" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="$(RazorPackageVersion)"/>
</ItemGroup>
<Target Name="BuildBlazorBuildBinary"
BeforeTargets="BlazorCompileRazorComponents">
<!-- Ensures this project is built before the consuming project, but without

View File

@ -1,4 +1,10 @@
<Project>
<!-- Require rebuild if the targets change -->
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup>
<BlazorBuildExe>dotnet &quot;$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.Build.dll&quot;</BlazorBuildExe>

View File

@ -1,27 +1,411 @@
<Project>
<Target Name="BlazorCompileRazorComponents" BeforeTargets="CoreCompile">
<PropertyGroup>
<BlazorComponentsNamespace>$(RootNamespace)</BlazorComponentsNamespace>
<IsDesignTimeBuild Condition="'$(DesignTimeBuild)' == 'true' OR '$(BuildingProject)' != 'true'">true</IsDesignTimeBuild>
<GeneratedFilePath>$(IntermediateOutputPath)BlazorRazorComponents.g.cs</GeneratedFilePath>
<SourceDir>$(ProjectDir.TrimEnd('\'))</SourceDir>
</PropertyGroup>
<Exec Command="$(BlazorBuildExe) buildrazor --source &quot;$(SourceDir)&quot; --namespace $(BlazorComponentsNamespace) --output &quot;$(GeneratedFilePath)&quot;" />
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
</ItemGroup>
</Target>
<Project>
<!-- Require rebuild if the targets change -->
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup>
<!-- Passed to Razor tasks and loaded as a plugin -->
<_BlazorAngleSharpAssemblyPath>$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.AngleSharp.dll</_BlazorAngleSharpAssemblyPath>
<_BlazorExtensionAssemblyPath>$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.Razor.Extensions.dll</_BlazorExtensionAssemblyPath>
<RazorDefaultConfiguration>Blazor-0.1</RazorDefaultConfiguration>
</PropertyGroup>
<!-- Something quick for input/output tracking - the assumptions here should match what the CLI does -->
<ItemGroup>
<BlazorGenerate Include="**\*.cshtml" />
<Content Update="@(Content->WithMetadataValue('Extension', '.cshtml'))">
<Generator>MSBuild:BlazorGenerateDeclaration</Generator>
</Content>
<ProjectCapability Include="DotNetCoreRazorConfiguration" />
<RazorConfiguration Include="Blazor-0.1">
<Extensions>Blazor-0.1;Blazor.AngleSharp-0.1;$(CustomRazorExtension)</Extensions>
</RazorConfiguration>
<RazorConfiguration Include="BlazorDeclaration-0.1">
<Extensions>Blazor-0.1;Blazor.AngleSharp-0.1;$(CustomRazorExtension)</Extensions>
</RazorConfiguration>
<RazorExtension Include="Blazor.AngleSharp-0.1">
<AssemblyName>Microsoft.AspNetCore.Blazor.AngleSharp</AssemblyName>
<AssemblyFilePath>$(_BlazorAngleSharpAssemblyPath)</AssemblyFilePath>
</RazorExtension>
<RazorExtension Include="Blazor-0.1">
<AssemblyName>Microsoft.AspNetCore.Blazor.Razor.Extensions</AssemblyName>
<AssemblyFilePath>$(_BlazorExtensionAssemblyPath)</AssemblyFilePath>
</RazorExtension>
<!-- Path used for the temporary compilation we produce for component discovery -->
<_BlazorTempAssembly Include="$(IntermediateOutputPath)$(TargetName).BlazorTemp.dll" />
</ItemGroup>
<ItemGroup>
<!-- Instruct VS to re-run the target when input files change. Other IDEs may not honor this
and therefore developers may need to rebuild after changing cshtml files. -->
<Compile Update="**\*.cshtml">
<Generator>MSBuild:BlazorCompileRazorComponents</Generator>
</Compile>
<!-- Instruct VS to include html/cshtml files in its "fast up-to-date check". If we didn't, then
it will skip the build entirely if you only changed html/cshtml files. -->
<UpToDateCheckInput Include="$(ProjectDir)**\*.cshtml" />
<UpToDateCheckInput Include="$(ProjectDir)**\*.html" />
</ItemGroup>
<PropertyGroup>
<BlazorGenerateDeclarationDependsOn>
_DefineBlazorPaths;
_AssignBlazorGenerateTargetPaths;
_HashBlazorGenerateInputs;
</BlazorGenerateDeclarationDependsOn>
<BlazorGenerateDefinitionDependsOn>
BlazorGenerateDeclaration;
BlazorResolveComponents;
</BlazorGenerateDefinitionDependsOn>
</PropertyGroup>
<!-- Defining properties that depend on import order in a target for resilience -->
<Target Name="_DefineBlazorPaths">
<PropertyGroup>
<!--
The Razor tasks require you to pass a tag helper manifest file. We don't need tag helpers for declarations
so that one is just a placeholder.
-->
<_BlazorGenerateDeclarationComponentManifest>$(IntermediateOutputPath)Blazor.declaration.components.json</_BlazorGenerateDeclarationComponentManifest>
<_BlazorGenerateDefinitionComponentManifest>$(IntermediateOutputPath)Blazor.definition.components.json</_BlazorGenerateDefinitionComponentManifest>
<_BlazorComponentInputCache>$(IntermediateOutputPath)Blazor.components.input.cache</_BlazorComponentInputCache>
</PropertyGroup>
</Target>
<!--
Assigns each BlazorGenerate item a relative path based on where the directory structure. This accounts
for <Link> and also for files outside of the project's folder hierarchy. So this is needed to support
linked files.
This step also assigns each item an output path for both stages of code generation.
-->
<Target Name="_AssignBlazorGenerateTargetPaths">
<AssignTargetPath Files="@(BlazorGenerate)" RootFolder="$(MSBuildProjectDirectory)">
<Output TaskParameter="AssignedFiles" ItemName="BlazorGenerateWithTargetPath" />
</AssignTargetPath>
<ItemGroup>
<BlazorGenerateWithTargetPath Condition="'%(BlazorGenerateWithTargetPath.GeneratedDeclaration)' == ''">
<GeneratedDeclaration>$(IntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(BlazorGenerateWithTargetPath.TargetPath)', 'g.i.cs'))</GeneratedDeclaration>
</BlazorGenerateWithTargetPath>
<BlazorGenerateWithTargetPath Condition="'%(BlazorGenerateWithTargetPath.GeneratedDefinition)' == ''">
<GeneratedDefinition>$(IntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(BlazorGenerateWithTargetPath.TargetPath)', 'g.cs'))</GeneratedDefinition>
</BlazorGenerateWithTargetPath>
</ItemGroup>
<!--
Instruct VS to re-run the target when input files change. Other IDEs may not honor this
and therefore developers may need to rebuild after changing cshtml files.
-->
<ItemGroup>
<BlazorDeclaration Include="@(BlazorGenerateWithTargetPath->'%(GeneratedDeclaration)')">
<DependentUpon>%(Identity)</DependentUpon>
</BlazorDeclaration>
</ItemGroup>
<ItemGroup>
<BlazorDefinition Include="@(BlazorGenerateWithTargetPath->'%(GeneratedDefinition)')" />
</ItemGroup>
</Target>
<Target Name="_HashBlazorGenerateInputs">
<PropertyGroup>
<!-- Used for input tracking -->
<_BlazorGenerateInputsHash></_BlazorGenerateInputsHash>
<_BlazorGenerateInputsHashFile>$(IntermediateOutputPath)Blazor.inputs.txt</_BlazorGenerateInputsHashFile>
</PropertyGroup>
<Hash ItemsToHash="@(BlazorGenerateWithTargetPath)">
<Output TaskParameter="HashResult" PropertyName="_BlazorGenerateInputsHash" />
</Hash>
<MakeDir
Directories="$(IntermediateOutputPath)"
Condition="!Exists('$(IntermediateOutputPath)')" />
<WriteLinesToFile
Lines="$(_BlazorGenerateInputsHash)"
File="$(_BlazorGenerateInputsHashFile)"
Overwrite="True"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(_BlazorGenerateInputsHashFile)" />
</ItemGroup>
</Target>
<!--
Generates 'declaration' files for each component, that only have that class and member declarations.
These files participate in the design-time-build for intellisense, and are used at build-time
when discovering components for a 'real' build.
-->
<Target
Name="BlazorGenerateDeclaration"
BeforeTargets="CoreCompile"
DependsOnTargets="$(BlazorGenerateDeclarationDependsOn)"
Inputs="$(MSBuildAllProjects);$(_BlazorExtensionAssemblyPath);$(_BlazorGenerateInputsHashFile);@(BlazorGenerate)"
Outputs="@(BlazorDeclaration)">
<ItemGroup>
<_BlazorGenerateDeclarationWithTargetPath Include="@(BlazorGenerateWithTargetPath)">
<GeneratedOutput>%(GeneratedDeclaration)</GeneratedOutput>
</_BlazorGenerateDeclarationWithTargetPath>
<_BlazorDeclarationConfiguration Include="@(RazorConfiguration->WithMetadataValue('Identity', 'BlazorDeclaration-0.1'))" />
</ItemGroup>
<MakeDir Directories="%(BlazorDeclaration.RelativeDir)" />
<RazorGenerate
Debug="$(_RazorDebugGenerateCodeTask)"
DebugTool="$(_RazorDebugGenerateCodeTool)"
ToolAssembly="$(_RazorToolAssembly)"
UseServer="$(UseRazorBuildServer)"
ForceServer="$(_RazorForceBuildServer)"
PipeName="$(_RazorBuildServerPipeName)"
Version="$(RazorLangVersion)"
Configuration="@(_BlazorDeclarationConfiguration)"
Extensions="@(RazorExtension)"
Sources="@(_BlazorGenerateDeclarationWithTargetPath)"
ProjectRoot="$(MSBuildProjectDirectory)"
TagHelperManifest="$(_BlazorGenerateDeclarationComponentManifest)" />
<ItemGroup>
<FileWrites Include="@(BlazorDeclaration)" />
</ItemGroup>
<ItemGroup Condition="'$(DesignTimeBuild)'=='true'">
<Compile Include="@(BlazorDeclaration)" />
</ItemGroup>
</Target>
<Target
Name="BlazorResolveComponents"
Inputs="$(MSBuildAllProjects);$(_BlazorExtensionAssemblyPath);@(ReferencePathWithRefAssemblies);@(_BlazorTempAssembly)"
Outputs="$(_RazorTagHelperInputCache)"
DependsOnTargets="_BlazorTempCompile">
<!-- Include the temp assembly as a reference so we can discover components from the app.-->
<ItemGroup>
<_BlazorReferencePathWithRefAssemblies Include="@(ReferencePathWithRefAssemblies);@(_BlazorTempAssembly)"/>
</ItemGroup>
<ItemGroup>
<_BlazorDeclarationConfiguration Include="@(RazorConfiguration->WithMetadataValue('Identity', 'BlazorDeclaration-0.1'))" />
</ItemGroup>
<!--
We're manipulating our output directly here because we want to separate the actual up-to-date check
of BlazorGenerateDefinition from the output of this target. Many times the set of components doesn't change
so we don't need to regenerate the code.
-->
<Touch
Files="$(_BlazorComponentInputCache)"
AlwaysCreate="true" />
<ItemGroup>
<FileWrites Include="$(_BlazorComponentInputCache)" />
</ItemGroup>
<RazorTagHelper
Debug="$(_RazorDebugTagHelperTask)"
DebugTool="$(_RazorDebugTagHelperTool)"
ToolAssembly="$(_RazorToolAssembly)"
UseServer="$(UseRazorBuildServer)"
ForceServer="$(_RazorForceBuildServer)"
PipeName="$(_RazorBuildServerPipeName)"
Version="$(RazorLangVersion)"
Configuration="@(_BlazorDeclarationConfiguration)"
Extensions="@(RazorExtension)"
Assemblies="@(_BlazorReferencePathWithRefAssemblies)"
ProjectRoot="$(MSBuildProjectDirectory)"
TagHelperManifest="$(_BlazorGenerateDefinitionComponentManifest)">
<Output
TaskParameter="TagHelperManifest"
ItemName="FileWrites"/>
</RazorTagHelper>
</Target>
<!--
Generates 'definition' files for each component, using the temp-compiled assembly as an input.
These files are used in the real build and don't participate in design time builds.
-->
<Target
Name="BlazorGenerateDefinition"
BeforeTargets="CoreCompile"
DependsOnTargets="$(BlazorGenerateDefinitionDependsOn)"
Inputs="$(MSBuildAllProjects);$(_BlazorExtensionAssemblyPath);$(_BlazorGenerateInputsHashFile);$(_BlazorGenerateDefinitionComponentManifest);@(BlazorGenerate)"
Outputs="@(BlazorDefinition)"
Condition="'$(DesignTimeBuild)'!='true'">
<ItemGroup>
<_BlazorGenerateDefinitionWithTargetPath Include="@(BlazorGenerateWithTargetPath)">
<GeneratedOutput>%(GeneratedDefinition)</GeneratedOutput>
</_BlazorGenerateDefinitionWithTargetPath>
<_BlazorDefinitionConfiguration Include="@(RazorConfiguration->WithMetadataValue('Identity', 'Blazor-0.1'))" />
</ItemGroup>
<MakeDir Directories="%(BlazorDefinition.RelativeDir)" />
<RazorGenerate
Debug="$(_RazorDebugGenerateCodeTask)"
DebugTool="$(_RazorDebugGenerateCodeTool)"
ToolAssembly="$(_RazorToolAssembly)"
UseServer="$(UseRazorBuildServer)"
ForceServer="$(_RazorForceBuildServer)"
PipeName="$(_RazorBuildServerPipeName)"
Version="$(RazorLangVersion)"
Configuration="@(_BlazorDefinitionConfiguration)"
Extensions="@(RazorExtension)"
Sources="@(_BlazorGenerateDefinitionWithTargetPath)"
ProjectRoot="$(MSBuildProjectDirectory)"
TagHelperManifest="$(_BlazorGenerateDefinitionComponentManifest)" />
<ItemGroup>
<FileWrites Include="@(BlazorDefinition)" />
</ItemGroup>
<ItemGroup Condition="'$(DesignTimeBuild)'!='true'">
<Compile Include="@(BlazorDefinition)" />
</ItemGroup>
</Target>
<!--
Taken from the Razor SDK targets
-->
<Target
Name="_BlazorTempCompile"
DependsOnTargets="BlazorGenerateDeclaration;FindReferenceAssembliesForReferences"
Inputs="
$(MSBuildAllProjects);
@(BlazorDeclaration);
@(Compile);
$(AssemblyOriginatorKeyFile);
@(ReferencePathWithRefAssemblies);
@(CompiledLicenseFile);
@(LinkResource);
$(ResolvedCodeAnalysisRuleSet);
@(AdditionalFiles)"
Outputs="@(_BlazorTempAssembly);$(NonExistentFile)"
Condition="'$(DesignTimeBuild)'!='true'">
<!-- These two compiler warnings are raised when a reference is bound to a different version
than specified in the assembly reference version number. MSBuild raises the same warning in this case,
so the compiler warning would be redundant. -->
<PropertyGroup Condition="('$(TargetFrameworkVersion)' != 'v1.0') and ('$(TargetFrameworkVersion)' != 'v1.1')">
<NoWarn>$(NoWarn);1701;1702</NoWarn>
</PropertyGroup>
<PropertyGroup>
<!-- To match historical behavior, when inside VS11+ disable the warning from csc.exe indicating that no sources were passed in-->
<NoWarn Condition="'$(BuildingInsideVisualStudio)' == 'true' AND '$(VisualStudioVersion)' != '' AND '$(VisualStudioVersion)' &gt; '10.0'">$(NoWarn);2008</NoWarn>
</PropertyGroup>
<ItemGroup Condition="'$(TargetingClr2Framework)' == 'true'">
<ReferencePathWithRefAssemblies>
<EmbedInteropTypes />
</ReferencePathWithRefAssemblies>
</ItemGroup>
<PropertyGroup>
<!-- If the user has specified AppConfigForCompiler, we'll use it. If they have not, but they set UseAppConfigForCompiler,
then we'll use AppConfig -->
<AppConfigForCompiler Condition="'$(AppConfigForCompiler)' == '' AND '$(UseAppConfigForCompiler)' == 'true'">$(AppConfig)</AppConfigForCompiler>
</PropertyGroup>
<!-- Prefer32Bit was introduced in .NET 4.5. Set it to false if we are targeting 4.0 -->
<PropertyGroup Condition="('$(TargetFrameworkVersion)' == 'v4.0')">
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<!-- TODO: Remove this ItemGroup once it has been moved to "_GenerateCompileInputs" target in Microsoft.Common.CurrentVersion.targets.
https://github.com/dotnet/roslyn/issues/12223 -->
<ItemGroup Condition="('$(AdditionalFileItemNames)' != '')">
<AdditionalFileItems Include="$(AdditionalFileItemNames)" />
<AdditionalFiles Include="@(%(AdditionalFileItems.Identity))" />
</ItemGroup>
<PropertyGroup Condition="'$(UseSharedCompilation)' == ''">
<UseSharedCompilation>true</UseSharedCompilation>
</PropertyGroup>
<Csc
AdditionalLibPaths="$(AdditionalLibPaths)"
AddModules="@(AddModules)"
AdditionalFiles="@(AdditionalFiles)"
AllowUnsafeBlocks="$(AllowUnsafeBlocks)"
Analyzers="@(Analyzer)"
ApplicationConfiguration="$(AppConfigForCompiler)"
BaseAddress="$(BaseAddress)"
CheckForOverflowUnderflow="$(CheckForOverflowUnderflow)"
ChecksumAlgorithm="$(ChecksumAlgorithm)"
CodeAnalysisRuleSet="$(ResolvedCodeAnalysisRuleSet)"
CodePage="$(CodePage)"
DebugType="$(DebugType)"
DefineConstants="$(DefineConstants)"
DelaySign="$(DelaySign)"
DisabledWarnings="$(NoWarn)"
EmitDebugInformation="$(DebugSymbols)"
EnvironmentVariables="$(CscEnvironment)"
ErrorEndLocation="$(ErrorEndLocation)"
ErrorLog="$(ErrorLog)"
ErrorReport="$(ErrorReport)"
Features="$(Features)"
FileAlignment="$(FileAlignment)"
GenerateFullPaths="$(GenerateFullPaths)"
HighEntropyVA="$(HighEntropyVA)"
Instrument="$(Instrument)"
KeyContainer="$(KeyContainerName)"
KeyFile="$(KeyOriginatorFile)"
LangVersion="$(LangVersion)"
LinkResources="@(LinkResource)"
MainEntryPoint="$(StartupObject)"
ModuleAssemblyName="$(ModuleAssemblyName)"
NoConfig="true"
NoLogo="$(NoLogo)"
NoStandardLib="$(NoCompilerStandardLib)"
NoWin32Manifest="$(NoWin32Manifest)"
Optimize="$(Optimize)"
Deterministic="$(Deterministic)"
PublicSign="$(PublicSign)"
OutputAssembly="@(_BlazorTempAssembly)"
Platform="$(PlatformTarget)"
Prefer32Bit="$(Prefer32Bit)"
PreferredUILang="$(PreferredUILang)"
ProvideCommandLineArgs="$(ProvideCommandLineArgs)"
References="@(ReferencePathWithRefAssemblies)"
ReportAnalyzer="$(ReportAnalyzer)"
Resources="@(CompiledLicenseFile)"
ResponseFiles="$(CompilerResponseFile)"
RuntimeMetadataVersion="$(RuntimeMetadataVersion)"
SharedCompilationId="$(SharedCompilationId)"
SkipCompilerExecution="$(SkipCompilerExecution)"
Sources="@(BlazorDeclaration);@(Compile)"
SubsystemVersion="$(SubsystemVersion)"
TargetType="$(OutputType)"
ToolExe="$(CscToolExe)"
ToolPath="$(CscToolPath)"
TreatWarningsAsErrors="$(TreatWarningsAsErrors)"
UseHostCompilerIfAvailable="$(UseHostCompilerIfAvailable)"
UseSharedCompilation="$(UseSharedCompilation)"
Utf8Output="$(Utf8Output)"
VsSessionGuid="$(VsSessionGuid)"
WarningLevel="$(WarningLevel)"
WarningsAsErrors="$(WarningsAsErrors)"
WarningsNotAsErrors="$(WarningsNotAsErrors)"
PathMap="$(PathMap)"
SourceLink="$(SourceLink)">
<Output TaskParameter="CommandLineArgs" ItemName="CscCommandLineArgs" />
</Csc>
<ItemGroup>
<FileWrites Include="@(_BlazorTempAssembly)" Condition="Exists('@(_BlazorTempAssembly)')" />
</ItemGroup>
</Target>
</Project>

View File

@ -25,7 +25,42 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
var method = documentNode.FindPrimaryMethod();
if (method == null)
{
return;
}
method.Children.Clear();
// After we clear all of the method body there might be some unused fields, which can be
// blocking if compiling with warnings as errors. Suppress this warning so that it doesn't
// get annoying in VS.
documentNode.Children.Insert(documentNode.Children.IndexOf(documentNode.FindPrimaryNamespace()), new CSharpCodeIntermediateNode()
{
Children =
{
// Field is assigned but never used
new IntermediateToken()
{
Content = "#pragma warning disable 0414" + Environment.NewLine,
Kind = TokenKind.CSharp,
},
// Field is never assigned
new IntermediateToken()
{
Content = "#pragma warning disable 0649" + Environment.NewLine,
Kind = TokenKind.CSharp,
},
// Field is never used
new IntermediateToken()
{
Content = "#pragma warning disable 0169" + Environment.NewLine,
Kind = TokenKind.CSharp,
},
},
});
}
}
}

View File

@ -0,0 +1,63 @@
// 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.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class ComponentDiscoveryRazorIntegrationTest : RazorIntegrationTestBase
{
internal override bool UseTwoPhaseCompilation => true;
[Fact]
public void ComponentDiscovery_CanFindComponent_DefinedinCSharp()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
using namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var result = CompileToCSharp("@addTagHelper *, TestAssembly");
// Assert
var bindings = result.CodeDocument.GetTagHelperContext();
Assert.Single(bindings.TagHelpers, t => t.Name == "Test.MyComponent");
}
[Fact]
public void ComponentDiscovery_CanFindComponent_DefinedinCshtml()
{
// Arrange
// Act
var result = CompileToCSharp("UniqueName.cshtml", "@addTagHelper *, TestAssembly");
// Assert
var bindings = result.CodeDocument.GetTagHelperContext();
Assert.Single(bindings.TagHelpers, t => t.Name == "Test.UniqueName");
}
[Fact]
public void ComponentDiscovery_CanFindComponent_BuiltIn()
{
// Arrange
// Act
var result = CompileToCSharp("@addTagHelper *, Microsoft.AspNetCore.Blazor");
// Assert
var bindings = result.CodeDocument.GetTagHelperContext();
Assert.Single(bindings.TagHelpers, t => t.Name == "Microsoft.AspNetCore.Blazor.Routing.NavLink");
}
}
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
using Xunit;
using Xunit.Sdk;
@ -26,10 +27,35 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test";
internal const string ArbitraryMacLinuxPath = "/dir/subdir/Test";
private RazorProjectEngine _projectEngine;
// Creating the initial compilation + reading references is on the order of 250ms without caching
// so making sure it doesn't happen for each test.
private static readonly CSharpCompilation BaseCompilation;
static RazorIntegrationTestBase()
{
var referenceAssemblyRoots = new[]
{
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime
typeof(BlazorComponent).Assembly,
typeof(RazorIntegrationTestBase).Assembly, // Reference this assembly, so that we can refer to test component types
};
var referenceAssemblies = referenceAssemblyRoots
.SelectMany(assembly => assembly.GetReferencedAssemblies().Concat(new[] { assembly.GetName() }))
.Distinct()
.Select(Assembly.Load)
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
.ToList();
BaseCompilation = CSharpCompilation.Create(
"TestAssembly",
Array.Empty<SyntaxTree>(),
referenceAssemblies,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
public RazorIntegrationTestBase()
{
AdditionalSyntaxTrees = new List<SyntaxTree>();
Configuration = BlazorExtensionInitializer.DefaultConfiguration;
FileSystem = new VirtualRazorProjectFileSystem();
WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath;
@ -38,6 +64,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
DefaultFileName = "TestComponent.cshtml";
}
internal List<SyntaxTree> AdditionalSyntaxTrees { get; }
internal virtual RazorConfiguration Configuration { get; }
internal virtual string DefaultBaseNamespace { get; }
@ -46,26 +74,21 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
internal virtual VirtualRazorProjectFileSystem FileSystem { get; }
internal virtual RazorProjectEngine ProjectEngine
{
get
{
if (_projectEngine == null)
{
_projectEngine = CreateProjectEngine();
}
return _projectEngine;
}
}
internal virtual bool UseTwoPhaseCompilation { get; }
internal virtual string WorkingDirectory { get; }
internal RazorProjectEngine CreateProjectEngine()
internal RazorProjectEngine CreateProjectEngine(RazorConfiguration configuration, MetadataReference[] references)
{
return RazorProjectEngine.Create(Configuration, FileSystem, b =>
return RazorProjectEngine.Create(configuration, FileSystem, b =>
{
BlazorExtensionInitializer.Register(b);
b.Features.Add(new CompilationTagHelperFeature());
b.Features.Add(new DefaultMetadataReferenceFeature()
{
References = references,
});
});
}
@ -95,13 +118,51 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
cshtmlRelativePath,
Encoding.UTF8.GetBytes(cshtmlContent));
var codeDocument = ProjectEngine.Process(projectItem);
return new CompileToCSharpResult
if (UseTwoPhaseCompilation)
{
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
};
// The first phase won't include any metadata references for component discovery. This mirrors
// what the build does.
var projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DeclarationConfiguration, Array.Empty<MetadataReference>());
var codeDocument = projectEngine.Process(projectItem);
// Result of generating declarations
var declaration = new CompileToCSharpResult
{
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
};
// Result of doing 'temp' compilation
var tempAssembly = CompileToAssembly(declaration, BaseCompilation);
// Add the 'temp' compilation as a metadata reference
var references = BaseCompilation.References.Concat(new[] { tempAssembly.Compilation.ToMetadataReference() }).ToArray();
projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DefaultConfiguration, references);
// Result of real code
codeDocument = projectEngine.Process(projectItem);
return new CompileToCSharpResult
{
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
};
}
else
{
// For single phase compilation tests just use the base compilation's references.
// This will include the built-in Blazor components.
var projectEngine = CreateProjectEngine(Configuration, BaseCompilation.References.ToArray());
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)
@ -115,8 +176,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
return CompileToAssembly(cSharpResult);
}
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult)
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, CSharpCompilation baseCompilation = null)
{
baseCompilation = baseCompilation ?? BaseCompilation;
if (cSharpResult.Diagnostics.Any())
{
var diagnosticsLog = string.Join(Environment.NewLine, cSharpResult.Diagnostics.Select(d => d.ToString()).ToArray());
@ -127,25 +190,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{
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);
var compilation = baseCompilation.AddSyntaxTrees(syntaxTrees).AddSyntaxTrees(AdditionalSyntaxTrees);
using (var peStream = new MemoryStream())
{
compilation.Emit(peStream);
@ -155,6 +201,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
.Where(d => d.Severity != DiagnosticSeverity.Hidden);
return new CompileToAssemblyResult
{
Compilation = compilation,
Diagnostics = diagnostics,
Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray())
};
@ -216,6 +263,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected class CompileToAssemblyResult
{
public Assembly Assembly { get; set; }
public Compilation Compilation { get; set; }
public string VerboseLog { get; set; }
public IEnumerable<Diagnostic> Diagnostics { get; set; }
}