diff --git a/sdk.sln b/sdk.sln index a5cea10f20a5..95184d771db8 100644 --- a/sdk.sln +++ b/sdk.sln @@ -264,15 +264,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.WorkloadM EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuiltInTools", "BuiltInTools", "{71A9F549-0EB6-41F9-BC16-4A6C5007FC91}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-watch", "src\BuiltInTools\dotnet-watch\dotnet-watch.csproj", "{445EFBD5-6730-4F09-943D-278E77501FFD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-watch", "src\BuiltInTools\dotnet-watch\dotnet-watch.csproj", "{445EFBD5-6730-4F09-943D-278E77501FFD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Watch.BrowserRefresh", "src\BuiltInTools\BrowserRefresh\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", "{A82EF2B9-24BC-4569-8FE5-9EF51017F4CB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh", "src\BuiltInTools\BrowserRefresh\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", "{A82EF2B9-24BC-4569-8FE5-9EF51017F4CB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-watch.Tests", "src\Tests\dotnet-watch.Tests\dotnet-watch.Tests.csproj", "{CCE1A328-9CFE-44D3-B68F-FE84A039ACEA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-watch.Tests", "src\Tests\dotnet-watch.Tests\dotnet-watch.Tests.csproj", "{CCE1A328-9CFE-44D3-B68F-FE84A039ACEA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Watch.BrowserRefresh.Tests", "src\Tests\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "{81ADA3FA-AC26-4149-8CFC-EC7808ECB820}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh.Tests", "src\Tests\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "{81ADA3FA-AC26-4149-8CFC-EC7808ECB820}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetWatchTasks", "src\BuiltInTools\DotNetWatchTasks\DotNetWatchTasks.csproj", "{A41DF752-6F21-4036-AD02-DD37B11A2723}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetWatchTasks", "src\BuiltInTools\DotNetWatchTasks\DotNetWatchTasks.csproj", "{A41DF752-6F21-4036-AD02-DD37B11A2723}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tasks", "src\BlazorWasmSdk\Tasks\Microsoft.NET.Sdk.BlazorWebAssembly.Tasks.csproj", "{4AE60971-C960-4DDF-B671-8B3E32C1AFD2}" EndProject @@ -330,6 +330,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rules", "Rules", "{A117DF29 src\RazorSdk\Targets\Rules\RazorGenerateWithTargetPath.xaml = src\RazorSdk\Targets\Rules\RazorGenerateWithTargetPath.xaml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{C2B15A41-A9C0-456A-A9FF-649E9237D850}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor.SourceGenerators", "src\RazorSdk\SourceGenerators\Microsoft.NET.Sdk.Razor.SourceGenerators.csproj", "{56C34654-DE8F-4F14-B2F8-6C37285B786E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -588,6 +592,10 @@ Global {08C9E634-39F3-4B24-BCEA-D0B21971EBBE}.Debug|Any CPU.Build.0 = Debug|Any CPU {08C9E634-39F3-4B24-BCEA-D0B21971EBBE}.Release|Any CPU.ActiveCfg = Release|Any CPU {08C9E634-39F3-4B24-BCEA-D0B21971EBBE}.Release|Any CPU.Build.0 = Release|Any CPU + {56C34654-DE8F-4F14-B2F8-6C37285B786E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56C34654-DE8F-4F14-B2F8-6C37285B786E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56C34654-DE8F-4F14-B2F8-6C37285B786E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56C34654-DE8F-4F14-B2F8-6C37285B786E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -696,6 +704,8 @@ Global {4ACCAC39-8ECD-45F8-B5AF-EB7E37CD36CC} = {E9BDA8A6-1AD2-4D0E-A37C-9EC10D7FE773} {D64884FD-A3F3-4082-A814-A9377B661509} = {E9BDA8A6-1AD2-4D0E-A37C-9EC10D7FE773} {A117DF29-1D72-453B-A24C-3B53F47609F2} = {D64884FD-A3F3-4082-A814-A9377B661509} + {C2B15A41-A9C0-456A-A9FF-649E9237D850} = {E9BDA8A6-1AD2-4D0E-A37C-9EC10D7FE773} + {56C34654-DE8F-4F14-B2F8-6C37285B786E} = {C2B15A41-A9C0-456A-A9FF-649E9237D850} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6} diff --git a/src/Assets/TestProjects/RazorMvcWithComponents/MvcWithComponents.csproj b/src/Assets/TestProjects/RazorMvcWithComponents/MvcWithComponents.csproj index fac9f14f4247..182c0c3521c5 100644 --- a/src/Assets/TestProjects/RazorMvcWithComponents/MvcWithComponents.csproj +++ b/src/Assets/TestProjects/RazorMvcWithComponents/MvcWithComponents.csproj @@ -9,8 +9,4 @@ false - - - - diff --git a/src/Assets/TestProjects/RazorMvcWithComponents/Views/Home/#FileName.cshtml b/src/Assets/TestProjects/RazorMvcWithComponents/Views/Home/FileName.cshtml similarity index 100% rename from src/Assets/TestProjects/RazorMvcWithComponents/Views/Home/#FileName.cshtml rename to src/Assets/TestProjects/RazorMvcWithComponents/Views/Home/FileName.cshtml diff --git a/src/Assets/TestProjects/RazorSimpleMvc50/SimpleMvc50.csproj b/src/Assets/TestProjects/RazorSimpleMvc50/SimpleMvc50.csproj index c526d91447ff..10508e09d6c8 100644 --- a/src/Assets/TestProjects/RazorSimpleMvc50/SimpleMvc50.csproj +++ b/src/Assets/TestProjects/RazorSimpleMvc50/SimpleMvc50.csproj @@ -1,7 +1,7 @@ - $(AspNetTestTfm) + net5.0 diff --git a/src/Layout/redist/targets/GenerateLayout.targets b/src/Layout/redist/targets/GenerateLayout.targets index 0fd35b2a2654..55aaf4ddf883 100644 --- a/src/Layout/redist/targets/GenerateLayout.targets +++ b/src/Layout/redist/targets/GenerateLayout.targets @@ -236,22 +236,31 @@ Targets="Publish" Projects="$(RepoRoot)/src/RazorSdk/Tool/Microsoft.NET.Sdk.Razor.Tool.csproj" Properties="Configuration=$(Configuration)" /> + <_RazorToolOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor.Tool\$(Configuration)\$(SdkTargetFramework)\publish\*.*" /> <_MvcRazorExtensionOutput Include="$(ArtifactsBinDir)$(Configuration)\Sdks\Microsoft.NET.Sdk.Razor\tasks\$(SdkTargetFramework)\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll" /> + <_RazorSourceGeneratorsOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor.SourceGenerators\$(Configuration)\netstandard2.0\publish\*.*" /> - + _action; + + public ConfigureRazorCodeGenerationOptions(Action action) + { + _action = action; + } + + public int Order { get; set; } + + public void Configure(RazorCodeGenerationOptionsBuilder options) => _action(options); + } +} diff --git a/src/RazorSdk/SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj b/src/RazorSdk/SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj new file mode 100644 index 000000000000..37cb96e592a0 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + MicrosoftAspNetCore + + + false + false + + + + + + + + + + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + + + + diff --git a/src/RazorSdk/SourceGenerators/RazorDiagnostics.cs b/src/RazorSdk/SourceGenerators/RazorDiagnostics.cs new file mode 100644 index 000000000000..61c92e07f2d8 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/RazorDiagnostics.cs @@ -0,0 +1,50 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal static class RazorDiagnostics + { + public static readonly DiagnosticDescriptor InvalidRazorLangVersionDescriptor = new DiagnosticDescriptor( +#pragma warning disable RS2008 // Enable analyzer release tracking + "RZ3600", +#pragma warning restore RS2008 // Enable analyzer release tracking + "Invalid RazorLangVersion", + "Invalid value {0} for RazorLangVersion. Valid values include 'Latest' or a valid version in range 1.0 to 5.0.", + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static Diagnostic AsDiagnostic(this RazorDiagnostic razorDiagnostic) + { + var descriptor = new DiagnosticDescriptor( + razorDiagnostic.Id, + razorDiagnostic.GetMessage(CultureInfo.CurrentCulture), + razorDiagnostic.GetMessage(CultureInfo.CurrentCulture), + "Razor", + razorDiagnostic.Severity switch + { + RazorDiagnosticSeverity.Error => DiagnosticSeverity.Error, + RazorDiagnosticSeverity.Warning => DiagnosticSeverity.Warning, + _ => DiagnosticSeverity.Hidden, + }, + isEnabledByDefault: true); + + var span = razorDiagnostic.Span; + var location = Location.Create( + span.FilePath, + span.AsTextSpan(), + new LinePositionSpan( + new LinePosition(span.LineIndex, span.CharacterIndex), + new LinePosition(span.LineIndex, span.CharacterIndex + span.Length))); + + return Diagnostic.Create(descriptor, location); + } + } +} diff --git a/src/RazorSdk/SourceGenerators/RazorInputItem.cs b/src/RazorSdk/SourceGenerators/RazorInputItem.cs new file mode 100644 index 000000000000..d7c109f612a0 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/RazorInputItem.cs @@ -0,0 +1,39 @@ +// 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.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal readonly struct RazorInputItem + { + public RazorInputItem(AdditionalText additionalText, string relativePath, string fileKind, string generatedOutputPath, string generatedDeclarationPath, string cssScope) + { + AdditionalText = additionalText; + RelativePath = relativePath; + CssScope = cssScope; + NormalizedPath = '/' + relativePath + .Replace(Path.DirectorySeparatorChar, '/') + .Replace("//", "/"); + FileKind = fileKind; + GeneratedOutputPath = generatedOutputPath; + GeneratedDeclarationPath = generatedDeclarationPath; + } + + public AdditionalText AdditionalText { get; } + + public string RelativePath { get; } + + public string NormalizedPath { get; } + + public string FileKind { get; } + + public string CssScope { get; } + + public string GeneratedOutputPath { get; } + + public string GeneratedDeclarationPath { get; } + } +} diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerationContext.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerationContext.cs new file mode 100644 index 000000000000..9e8e064503ac --- /dev/null +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerationContext.cs @@ -0,0 +1,197 @@ +// 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.Diagnostics; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal class RazorSourceGenerationContext + { + public string RootNamespace { get; private set; } + + public IReadOnlyList RazorFiles { get; private set; } + + public IReadOnlyList CshtmlFiles { get; private set; } + + public VirtualRazorProjectFileSystem FileSystem { get; private set; } + + public RazorConfiguration Configuration { get; private set; } + + public bool DesignTimeBuild { get; private set; } + + public string RefsTagHelperOutputCachePath { get; private set; } + + /// + /// Gets a flag that determines if the source generator waits for the debugger to attach. + /// + /// To configure this using MSBuild, use the _RazorSourceGeneratorDebug property. + /// For instance dotnet msbuild /p:_RazorSourceGeneratorDebug=true + /// + /// + public bool WaitForDebugger { get; private set; } + + /// + /// Gets a flag that determines if generated output is to be written to disk. + /// Primarily meant for tests and debugging. + /// + /// To configure this using MSBuild, use the _RazorSourceGeneratorWriteGeneratedOutput property. + /// For instance dotnet msbuild /p:_RazorSourceGeneratorWriteGeneratedOutput=true + /// + /// + public bool WriteGeneratedContent { get; private set; } + + /// + /// Gets a flag that determine if the source generator should log verbose messages. + /// + /// + /// To configure this using MSBuild, use the _RazorSourceGeneratorLog property. + /// For instance dotnet msbuild /p:_RazorSourceGeneratorLog=true + /// + public bool EnableLogging { get; private set; } + + public static RazorSourceGenerationContext Create(GeneratorExecutionContext context) + { + var globalOptions = context.AnalyzerConfigOptions.GlobalOptions; + + if (!globalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace)) + { + rootNamespace = "ASP"; + } + + globalOptions.TryGetValue("build_property.DesignTimeBuild", out var designTimeBuild); + if (!globalOptions.TryGetValue("build_property._RazorReferenceAssemblyTagHelpersOutputPath", out var refsTagHelperOutputCachePath)) + { + throw new InvalidOperationException("_RazorReferenceAssemblyTagHelpersOutputPath is not specified."); + } + + if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) || + !RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion)) + { + context.ReportDiagnostic(Diagnostic.Create( + RazorDiagnostics.InvalidRazorLangVersionDescriptor, + Location.None, + razorLanguageVersionString)); + + return null; + } + + if (!globalOptions.TryGetValue("build_property.RazorConfiguration", out var configurationName)) + { + configurationName = "default"; + } + + globalOptions.TryGetValue("build_property._RazorSourceGeneratorDebug", out var waitForDebugger); + globalOptions.TryGetValue("build_property._RazorSourceGeneratorWriteGeneratedOutput", out var writeOutput); + globalOptions.TryGetValue("build_property._RazorSourceGeneratorLog", out var enableLogging); + + var razorConfiguration = RazorConfiguration.Create(razorLanguageVersion, configurationName, Enumerable.Empty()); + var (razorFiles, cshtmlFiles) = GetRazorInputs(context); + var fileSystem = GetVirtualFileSystem(razorFiles, cshtmlFiles); + + return new RazorSourceGenerationContext + { + RootNamespace = rootNamespace, + Configuration = razorConfiguration, + FileSystem = fileSystem, + RazorFiles = razorFiles, + CshtmlFiles = cshtmlFiles, + DesignTimeBuild = designTimeBuild == "true", + RefsTagHelperOutputCachePath = refsTagHelperOutputCachePath, + WaitForDebugger = waitForDebugger == "true", + WriteGeneratedContent = writeOutput == "true", + EnableLogging = enableLogging == "true" + }; + } + + private static VirtualRazorProjectFileSystem GetVirtualFileSystem(IReadOnlyList razorFiles, IReadOnlyList cshtmlFiles) + { + var fileSystem = new VirtualRazorProjectFileSystem(); + for (var i = 0; i < razorFiles.Count; i++) + { + var item = razorFiles[i]; + fileSystem.Add(new SourceGeneratorProjectItem( + basePath: "/", + filePath: item.NormalizedPath, + relativePhysicalPath: item.RelativePath, + fileKind: FileKinds.Component, + item.AdditionalText, + cssScope: item.CssScope)); + } + + for (var i = 0; i < cshtmlFiles.Count; i++) + { + var item = cshtmlFiles[i]; + fileSystem.Add(new SourceGeneratorProjectItem( + basePath: "/", + filePath: item.NormalizedPath, + relativePhysicalPath: item.RelativePath, + fileKind: FileKinds.Legacy, + item.AdditionalText, + cssScope: item.CssScope)); + } + + return fileSystem; + } + + private static (IReadOnlyList razorFiles, IReadOnlyList cshtmlFiles) GetRazorInputs(GeneratorExecutionContext context) + { + List razorFiles = null; + List cshtmlFiles = null; + + foreach (var item in context.AdditionalFiles) + { + var path = item.Path; + var isComponent = path.EndsWith(".razor", StringComparison.OrdinalIgnoreCase); + var isRazorView = path.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase); + + if (!isComponent && !isRazorView) + { + continue; + } + + var options = context.AnalyzerConfigOptions.GetOptions(item); + if (!options.TryGetValue("build_metadata.AdditionalFiles.TargetPath", out var relativePath)) + { + throw new InvalidOperationException($"TargetPath is not specified for additional file '{item.Path}."); + } + + options.TryGetValue("build_metadata.AdditionalFiles.CssScope", out var cssScope); + + string generatedDeclarationPath = null; + + options.TryGetValue("build_metadata.AdditionalFiles.GeneratedOutputFullPath", out var generatedOutputPath); + if (isComponent) + { + options.TryGetValue("build_metadata.AdditionalFiles.GeneratedDeclarationFullPath", out generatedDeclarationPath); + } + + var fileKind = isComponent ? FileKinds.GetComponentFileKindFromFilePath(item.Path) : FileKinds.Legacy; + + var inputItem = new RazorInputItem(item, relativePath, fileKind, generatedOutputPath, generatedDeclarationPath, cssScope); + + if (isComponent) + { + razorFiles ??= new(); + razorFiles.Add(inputItem); + } + else + { + cshtmlFiles ??= new(); + cshtmlFiles.Add(inputItem); + } + } + + return ( + (IReadOnlyList)razorFiles ?? Array.Empty(), + (IReadOnlyList)cshtmlFiles ?? Array.Empty() + ); + } + + } +} diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs new file mode 100644 index 000000000000..06c67440db84 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs @@ -0,0 +1,280 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + [Generator] + public partial class RazorSourceGenerator : ISourceGenerator + { + private static readonly ParallelOptions DefaultParallelOptions = new(); + + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + var razorContext = RazorSourceGenerationContext.Create(context); + if (razorContext is null || + (razorContext.RazorFiles.Count == 0 && razorContext.CshtmlFiles.Count == 0)) + { + return; + } + + HandleDebugSwitch(razorContext.WaitForDebugger); + + var tagHelpers = ResolveTagHelperDescriptors(context, razorContext); + + var projectEngine = RazorProjectEngine.Create(razorContext.Configuration, razorContext.FileSystem, b => + { + b.Features.Add(new DefaultTypeNameFeature()); + b.SetRootNamespace(razorContext.RootNamespace); + + b.Features.Add(new StaticTagHelperFeature { TagHelpers = tagHelpers, }); + b.Features.Add(new DefaultTagHelperDescriptorProvider()); + + CompilerFeatures.Register(b); + RazorExtensions.Register(b); + + b.SetCSharpLanguageVersion(((CSharpParseOptions)context.ParseOptions).LanguageVersion); + }); + + CodeGenerateRazorComponents(context, razorContext, projectEngine); + GenerateViews(context, razorContext, projectEngine); + } + + private void GenerateViews(GeneratorExecutionContext context, RazorSourceGenerationContext razorContext, RazorProjectEngine projectEngine) + { + var files = razorContext.CshtmlFiles; + + Parallel.For(0, files.Count, GetParallelOptions(), i => + { + var file = files[i]; + + var codeDocument = projectEngine.Process(projectEngine.FileSystem.GetItem(file.NormalizedPath, FileKinds.Legacy)); + var csharpDocument = codeDocument.GetCSharpDocument(); + for (var j = 0; j < csharpDocument.Diagnostics.Count; j++) + { + var razorDiagnostic = csharpDocument.Diagnostics[j]; + var csharpDiagnostic = razorDiagnostic.AsDiagnostic(); + context.ReportDiagnostic(csharpDiagnostic); + } + + Directory.CreateDirectory(Path.GetDirectoryName(file.GeneratedOutputPath)); + File.WriteAllText(file.GeneratedOutputPath, csharpDocument.GeneratedCode); + }); + } + + private static void CodeGenerateRazorComponents(GeneratorExecutionContext context, RazorSourceGenerationContext razorContext, RazorProjectEngine projectEngine) + { + var files = razorContext.RazorFiles; + + var arraypool = ArrayPool<(string, SourceText)>.Shared; + var outputs = arraypool.Rent(files.Count); + + Parallel.For(0, files.Count, GetParallelOptions(), i => + { + var file = files[i]; + var projectItem = projectEngine.FileSystem.GetItem(file.NormalizedPath, FileKinds.Component); + + var codeDocument = projectEngine.Process(projectItem); + var csharpDocument = codeDocument.GetCSharpDocument(); + for (var j = 0; j < csharpDocument.Diagnostics.Count; j++) + { + var razorDiagnostic = csharpDocument.Diagnostics[j]; + var csharpDiagnostic = razorDiagnostic.AsDiagnostic(); + context.ReportDiagnostic(csharpDiagnostic); + } + + var hint = GetIdentifierFromPath(file.NormalizedPath); + + var generatedCode = csharpDocument.GeneratedCode; + if (razorContext.WriteGeneratedContent) + { + var path = file.GeneratedOutputPath; + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllText(path, generatedCode); + } + + outputs[i] = (hint, SourceText.From(generatedCode, Encoding.UTF8)); + }); + + for (var i = 0; i < files.Count; i++) + { + var (hint, sourceText) = outputs[i]; + context.AddSource(hint, sourceText); + } + + arraypool.Return(outputs); + } + + private static IReadOnlyList ResolveTagHelperDescriptors(GeneratorExecutionContext GeneratorExecutionContext, RazorSourceGenerationContext razorContext) + { + var tagHelperFeature = new StaticCompilationTagHelperFeature(); + + var langVersion = ((CSharpParseOptions)GeneratorExecutionContext.ParseOptions).LanguageVersion; + + var discoveryProjectEngine = RazorProjectEngine.Create(razorContext.Configuration, razorContext.FileSystem, b => + { + b.Features.Add(new DefaultTypeNameFeature()); + b.Features.Add(new ConfigureRazorCodeGenerationOptions(options => + { + options.SuppressPrimaryMethodBody = true; + options.SuppressChecksum = true; + })); + + b.SetRootNamespace(razorContext.RootNamespace); + + var metadataReferences = new List(GeneratorExecutionContext.Compilation.References); + b.Features.Add(new DefaultMetadataReferenceFeature { References = metadataReferences }); + + b.Features.Add(tagHelperFeature); + b.Features.Add(new DefaultTagHelperDescriptorProvider()); + + CompilerFeatures.Register(b); + RazorExtensions.Register(b); + + b.SetCSharpLanguageVersion(langVersion); + }); + + var files = razorContext.RazorFiles; + var results = ArrayPool.Shared.Rent(files.Count); + + var parseOptions = (CSharpParseOptions)GeneratorExecutionContext.ParseOptions; + + Parallel.For(0, files.Count, GetParallelOptions(), i => + { + var file = files[i]; + if (File.GetLastWriteTimeUtc(file.GeneratedDeclarationPath) > File.GetLastWriteTimeUtc(file.AdditionalText.Path)) + { + // Declaration files are invariant to other razor files, tag helpers, assemblies. If we have previously generated + // content that it's still newer than the output file, use it and save time processing the file. + using var outputFileStream = File.OpenRead(file.GeneratedDeclarationPath); + results[i] = CSharpSyntaxTree.ParseText( + SourceText.From(outputFileStream), + options: parseOptions); + } + else + { + var codeGen = discoveryProjectEngine.Process(discoveryProjectEngine.FileSystem.GetItem(file.NormalizedPath, FileKinds.Component)); + var generatedCode = codeGen.GetCSharpDocument().GeneratedCode; + + Directory.CreateDirectory(Path.GetDirectoryName(file.GeneratedDeclarationPath)); + File.WriteAllText(file.GeneratedDeclarationPath, generatedCode); + + results[i] = CSharpSyntaxTree.ParseText( + generatedCode, + options: parseOptions); + } + }); + + tagHelperFeature.Compilation = GeneratorExecutionContext.Compilation.AddSyntaxTrees(results.Take(files.Count)); + ArrayPool.Shared.Return(results); + + var lastUpdatedReferenceUtc = GetLastUpdatedReference(GeneratorExecutionContext.Compilation.References); + IReadOnlyList refTagHelpers; + + if (lastUpdatedReferenceUtc is not null && lastUpdatedReferenceUtc < File.GetLastWriteTimeUtc(razorContext.RefsTagHelperOutputCachePath)) + { + // Producing tag helpers from a Compilation every time is surprisingly expensive. So we'll use some caching strategies to mitigate this until + // we can improve the perf in that area. + + // TagHelpers can come from two locations - the declaration files + // and the assemblies participating in the compilation. + // In a typical inner loop, the assemblies referenced by the project do not change. We could cache these separately from the tag helpers produced + // by the app to avoid some per-compilation costs. + // We determine if any of the reference assemblies have a newer timestamp than the output cache for the tag helpers. If not, we can re-use previously + // calculated results. + refTagHelpers = TagHelperSerializer.Deserialize(razorContext.RefsTagHelperOutputCachePath); + } + else + { + tagHelperFeature.DiscoveryMode = TagHelperDiscoveryFilter.ReferenceAssemblies; + refTagHelpers = tagHelperFeature.GetDescriptors(); + TagHelperSerializer.Serialize(razorContext.RefsTagHelperOutputCachePath, refTagHelpers); + } + + tagHelperFeature.DiscoveryMode = TagHelperDiscoveryFilter.CurrentCompilation; + var assemblyTagHelpers = tagHelperFeature.GetDescriptors(); + + var result = new List(refTagHelpers.Count + assemblyTagHelpers.Count); + result.AddRange(assemblyTagHelpers); + result.AddRange(refTagHelpers); + + return result; + } + + private static DateTime? GetLastUpdatedReference(IEnumerable references) + { + DateTime lastWriteTimeUtc = DateTime.MinValue; + + foreach (var reference in references) + { + // We expect all references in the compilation context to be backed by a file on disk. If not, + // we'll bail out and regenerate the tag helper cache where this is invoked. + if (reference is not PortableExecutableReference portableExecutableReference || string.IsNullOrEmpty(portableExecutableReference.FilePath)) + { + return null; + } + + var fileWriteTime = File.GetLastWriteTimeUtc(portableExecutableReference.FilePath); + + lastWriteTimeUtc = lastWriteTimeUtc < fileWriteTime ? fileWriteTime : lastWriteTimeUtc; + } + + return lastWriteTimeUtc; + } + + private static string GetIdentifierFromPath(string filePath) + { + var builder = new StringBuilder(filePath.Length); + + for (var i = 0; i < filePath.Length; i++) + { + builder.Append(filePath[i] switch + { + ':' or '\\' or '/' => '_', + var @default => @default, + }); + } + + return builder.ToString(); + } + + private static ParallelOptions GetParallelOptions() + { + if (Debugger.IsAttached) + { + return new ParallelOptions { MaxDegreeOfParallelism = 1 }; + } + return DefaultParallelOptions; + } + + private static void HandleDebugSwitch(bool waitForDebugger) + { + if (waitForDebugger) + { + while (!Debugger.IsAttached) + { + Thread.Sleep(3000); + } + } + } + } +} diff --git a/src/RazorSdk/SourceGenerators/SourceGeneratorProjectItem.cs b/src/RazorSdk/SourceGenerators/SourceGeneratorProjectItem.cs new file mode 100644 index 000000000000..947caf5639a5 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/SourceGeneratorProjectItem.cs @@ -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; +using System.Text; +using System.IO; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal class SourceGeneratorProjectItem : RazorProjectItem + { + private readonly string _fileKind; + + public SourceGeneratorProjectItem(string basePath, string filePath, string relativePhysicalPath, string fileKind, AdditionalText additionalText, string cssScope) + { + BasePath = basePath; + FilePath = filePath; + RelativePhysicalPath = relativePhysicalPath; + _fileKind = fileKind; + AdditionalText = additionalText; + CssScope = cssScope; + var text = AdditionalText.GetText(); + RazorSourceDocument = new SourceTextRazorSourceDocument(AdditionalText.Path, relativePhysicalPath, text); + } + + public AdditionalText AdditionalText { get; } + + public override string BasePath { get; } + + public override string FilePath { get; } + + public override bool Exists => true; + + public override string PhysicalPath => AdditionalText.Path; + + public override string RelativePhysicalPath { get; } + + public override string FileKind => _fileKind ?? base.FileKind; + + public override string CssScope { get; } + + public override Stream Read() + => throw new NotSupportedException("This API should not be invoked. We should instead be relying on " + + "the RazorSourceDocument associated with this item instead."); + } +} diff --git a/src/RazorSdk/SourceGenerators/SourceTextRazorSourceDocument.cs b/src/RazorSdk/SourceGenerators/SourceTextRazorSourceDocument.cs new file mode 100644 index 000000000000..7b1b12e57d64 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/SourceTextRazorSourceDocument.cs @@ -0,0 +1,70 @@ +// 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.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal class SourceTextRazorSourceDocument : RazorSourceDocument + { + private readonly SourceText _sourceText; + + public SourceTextRazorSourceDocument(string filePath, string relativePath, SourceText sourceText) + { + FilePath = filePath; + RelativePath = relativePath; + _sourceText = sourceText; + Lines = new SourceTextSourceLineCollection(filePath, sourceText.Lines); + } + + public override char this[int position] => _sourceText[position]; + + public override Encoding Encoding => _sourceText.Encoding; + + public override string FilePath { get; } + + public override int Length => _sourceText.Length; + + public override string RelativePath { get; } + + public override RazorSourceLineCollection Lines { get; } + + public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + _sourceText.CopyTo(sourceIndex, destination, destinationIndex, count); + } + + public override byte[] GetChecksum() => _sourceText.GetChecksum().ToArray(); + + public override string GetChecksumAlgorithm() => _sourceText.ChecksumAlgorithm.ToString().ToUpperInvariant(); + + private class SourceTextSourceLineCollection : RazorSourceLineCollection + { + private readonly string _filePath; + private readonly TextLineCollection _textLines; + + public SourceTextSourceLineCollection(string filePath, TextLineCollection textLines) + { + _filePath = filePath; + _textLines = textLines; + } + + public override int Count => _textLines.Count; + + public override int GetLineLength(int index) + { + var line = _textLines[index]; + return line.EndIncludingLineBreak - line.Start; + } + + internal override SourceLocation GetLocation(int position) + { + var line = _textLines.GetLineFromPosition(position); + return new SourceLocation(_filePath, position, line.LineNumber, position); + } + } + } +} diff --git a/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs b/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs new file mode 100644 index 000000000000..ef683579f33a --- /dev/null +++ b/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs @@ -0,0 +1,56 @@ +// 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.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal sealed class StaticCompilationTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature + { + private ITagHelperDescriptorProvider[] _providers; + + public IReadOnlyList GetDescriptors() + { + if (Compilation is null) + { + return Array.Empty(); + } + + var results = new List(); + + + var context = TagHelperDescriptorProviderContext.Create(results); + context.SetCompilation(Compilation); + + for (var i = 0; i < _providers.Length; i++) + { + _providers[i].Execute(context); + } + + return results; + } + + public Compilation Compilation { get; set; } + + public TagHelperDiscoveryFilter DiscoveryMode { get; set; } + + protected override void OnInitialized() + { + _providers = Engine.Features.OfType().OrderBy(f => f.Order).ToArray(); + } + + internal static bool IsValidCompilation(Compilation compilation) + { + var @string = compilation.GetSpecialType(SpecialType.System_String); + + // Do some minimal tests to verify the compilation is valid. If symbols for System.String + // is missing or errored, the compilation may be missing references. + return @string != null && @string.TypeKind != TypeKind.Error; + } + } +} diff --git a/src/RazorSdk/SourceGenerators/StaticTagHelperFeature.cs b/src/RazorSdk/SourceGenerators/StaticTagHelperFeature.cs new file mode 100644 index 000000000000..19eede74f63f --- /dev/null +++ b/src/RazorSdk/SourceGenerators/StaticTagHelperFeature.cs @@ -0,0 +1,17 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal sealed class StaticTagHelperFeature : ITagHelperFeature + { + public RazorEngine Engine { get; set; } + + public IReadOnlyList TagHelpers { get; set; } + + public IReadOnlyList GetDescriptors() => TagHelpers; + } +} diff --git a/src/RazorSdk/SourceGenerators/TagHelperSerializer.cs b/src/RazorSdk/SourceGenerators/TagHelperSerializer.cs new file mode 100644 index 000000000000..8ee27a906693 --- /dev/null +++ b/src/RazorSdk/SourceGenerators/TagHelperSerializer.cs @@ -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.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators +{ + internal class TagHelperSerializer + { + private static readonly JsonSerializer Serializer = new JsonSerializer + { + Converters = + { + new TagHelperDescriptorJsonConverter(), + new RazorDiagnosticJsonConverter(), + } + }; + + public static void Serialize(string manifestFilePath, IReadOnlyList tagHelpers) + { + using var stream = File.OpenWrite(manifestFilePath); + using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true); + + Serializer.Serialize(writer, tagHelpers); + } + + public static IReadOnlyList Deserialize(string manifestFilePath) + { + using var stream = File.OpenRead(manifestFilePath); + using var reader = new JsonTextReader(new StreamReader(stream)); + + return Serializer.Deserialize>(reader); + } + } +} diff --git a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.CodeGeneration.targets b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.CodeGeneration.targets index 76c4bba7c972..e06d9b9d58ff 100644 --- a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.CodeGeneration.targets +++ b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.CodeGeneration.targets @@ -171,15 +171,7 @@ Copyright (c) .NET Foundation. All rights reserved. - $(ResolveRazorCompileInputsDependsOn);_ResolveGeneratedRazorCompileInputs + $(ResolveRazorCompileInputsDependsOn) - - - - - - diff --git a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets index c512c7ee49f2..fd86f896df7f 100644 --- a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets +++ b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets @@ -66,8 +66,10 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + true @@ -156,7 +164,7 @@ Copyright (c) .NET Foundation. All rights reserved. Prefer32Bit="$(Prefer32Bit)" PreferredUILang="$(PreferredUILang)" ProvideCommandLineArgs="$(ProvideCommandLineArgs)" - References="@(RazorReferencePath)" + References="@(ReferencePath);@(IntermediateAssembly)" ReportAnalyzer="$(ReportAnalyzer)" Resources="@(_RazorCoreCompileResourceInputs);@(CompiledLicenseFile)" ResponseFiles="$(CompilerResponseFile)" diff --git a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.SourceGenerators.targets b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.SourceGenerators.targets new file mode 100644 index 000000000000..bf0d32a29d8d --- /dev/null +++ b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.SourceGenerators.targets @@ -0,0 +1,69 @@ + + + + + + + + <_RazorSdkSourceGeneratorDirectoryRoot>$(RazorSdkDirectoryRoot)\source-generators\ + <_RazorReferenceAssemblyTagHelpersOutputPath>$([System.IO.Path]::GetFullPath($(IntermediateOutputPath)))RazorTagHelper.refs.out.cache + + + + + <_RazorAnalyzer Include="$(_RazorSdkSourceGeneratorDirectoryRoot)Newtonsoft.Json.dll" /> + <_RazorAnalyzer Include="$(_RazorSdkSourceGeneratorDirectoryRoot)Microsoft.AspNetCore.Mvc.Razor.Extensions.dll" /> + <_RazorAnalyzer Include="$(_RazorSdkSourceGeneratorDirectoryRoot)Microsoft.AspNetCore.Razor.Language.dll" /> + <_RazorAnalyzer Include="$(_RazorSdkSourceGeneratorDirectoryRoot)Microsoft.CodeAnalysis.Razor.dll" /> + <_RazorAnalyzer Include="$(_RazorSdkSourceGeneratorDirectoryRoot)Microsoft.NET.Sdk.Razor.SourceGenerators.dll" /> + + + + + + + + + + + + + + + + + + + + + + + + + + <_RazorAdditionalFile Include="@(RazorComponentWithTargetPath)" /> + + <_RazorAdditionalFile Include="@(RazorGenerateWithTargetPath)" Condition="'$(RazorCompileOnBuild)' != 'false'" /> + + + + + + + + + + + diff --git a/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets b/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets index 45410b321bc3..bdface52f55e 100644 --- a/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets +++ b/src/RazorSdk/Targets/Sdk.Razor.CurrentVersion.targets @@ -42,19 +42,36 @@ Copyright (c) .NET Foundation. All rights reserved. <_RazorSdkDotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe - + + + + + <_TargetingNETCoreApp30OrLater>true + <_TargetingNET50OrLater>true + <_TargetingNET60OrLater>true + <_UseRazorSourceGenerator>true + 5.0 + + + + + <_TargetingNETCoreApp30OrLater>true + <_TargetingNET50OrLater>true + <_UseRazorSourceGenerator>false + 5.0 + + + + + <_TargetingNETCoreApp30OrLater>true + <_UseRazorSourceGenerator>false + 3.0 + + + + + - <_TargetingNETCoreApp30OrLater Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND - $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '3.0')) ">true - <_TargetingNET50OrLater Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND - $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '5.0')) ">true - - - 5.0 - 3.0 - - + - + + + @@ -597,17 +624,6 @@ Copyright (c) .NET Foundation. All rights reserved. - - - - - - - - - + + - + @@ -879,6 +891,14 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + diff --git a/src/Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs b/src/Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs index 07c848fee3f9..12e6a3d49a8f 100644 --- a/src/Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs +++ b/src/Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs @@ -168,7 +168,6 @@ public void Publish_UpdatesServiceWorkerVersionHash_WhenSourcesChange() var capture = match.Groups[1].Value; // Act - System.Console.WriteLine(testInstance.TestRoot); var cssFile = Path.Combine(testInstance.TestRoot, "blazorwasm", "LinkToWebRoot", "css", "app.css"); File.WriteAllText(cssFile, ".updated { }"); diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIncrementalismTest.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIncrementalismTest.cs index 55e79e974237..735b54fd72a2 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIncrementalismTest.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIncrementalismTest.cs @@ -72,42 +72,7 @@ public void BuildIncremental_SimpleMvc_PersistsTargetInputFile() } } - [Fact] - public void RazorGenerate_RegeneratesTagHelperInputs_IfFileChanges() - { - var testAsset = "RazorSimpleMvc"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - // Act - 1 - var build = new BuildCommand(projectDirectory); - var result = build.Execute(); - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var expectedTagHelperCacheContent = @"""Name"":""SimpleMvc.SimpleTagHelper"""; - var file = Path.Combine(projectDirectory.Path, "SimpleTagHelper.cs"); - var tagHelperOutputCache = Path.Combine(intermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"); - var generatedFile = Path.Combine(intermediateOutputPath, "Razor", "Views", "Home", "Index.cshtml.g.cs"); - - // Assert - 1 - result.Should().Pass(); - new FileInfo(tagHelperOutputCache).Should().Contain(expectedTagHelperCacheContent); - var fileThumbPrint = FileThumbPrint.Create(generatedFile); - - // Act - 2 - // Update the source content and build. We should expect the outputs to be regenerated. - File.WriteAllText(file, string.Empty); - build = new BuildCommand(projectDirectory); - result = build.Execute(); - - // Assert - 2 - result.Should().Pass(); - new FileInfo(tagHelperOutputCache).Should().NotContain(@"""Name"":""SimpleMvc.SimpleTagHelper"""); - var newThumbPrint = FileThumbPrint.Create(generatedFile); - Assert.NotEqual(fileThumbPrint, newThumbPrint); - } - - [Fact] + [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/28780")] public void Build_ErrorInGeneratedCode_ReportsMSBuildError_OnIncrementalBuild() { var testAsset = "RazorSimpleMvc"; @@ -175,7 +140,7 @@ void VerifyError(TestAsset projectDirectory) } } - [Fact] + [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/28780")] public void BuildComponents_DoesNotRegenerateComponentDefinition_WhenDefinitionIsUnchanged() { var testAsset = "RazorMvcWithComponents"; @@ -232,120 +197,6 @@ public void BuildComponents_DoesNotRegenerateComponentDefinition_WhenDefinitionI Assert.Equal(definitionThumbprint, FileThumbPrint.Create(tagHelperOutputCache)); } - [Fact] - public void BuildComponents_RegeneratesComponentDefinition_WhenFilesChange() - { - var testAsset = "RazorMvcWithComponents"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - var build = new BuildCommand(projectDirectory); - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - // Act - 1 - var updatedContent = "@code { [Parameter] public string AParameter { get; set; } }"; - var tagHelperOutputCache = Path.Combine(intermediateOutputPath, "MvcWithComponents.TagHelpers.output.cache"); - - var generatedFile = Path.Combine(intermediateOutputPath, "Razor", "Views", "Shared", "NavMenu.razor.g.cs"); - var generatedDefinitionFile = Path.Combine(intermediateOutputPath, "RazorDeclaration", "Views", "Shared", "NavMenu.razor.g.cs"); - - // Assert - 1 - var result = build.Execute(); - - result.Should().Pass(); - var outputFile = Path.Combine(outputPath, "MvcWithComponents.dll"); - new FileInfo(outputFile).Should().Exist(); - var outputAssemblyThumbprint = FileThumbPrint.Create(outputFile); - - new FileInfo(generatedDefinitionFile).Should().Exist(); - var generatedDefinitionThumbprint = FileThumbPrint.Create(generatedDefinitionFile); - new FileInfo(generatedFile).Should().Exist(); - var generatedFileThumbprint = FileThumbPrint.Create(generatedFile); - - new FileInfo(tagHelperOutputCache).Should().Exist(); - new FileInfo(tagHelperOutputCache).Should().Contain(@"""Name"":""MvcWithComponents.Views.Shared.NavMenu"""); - - var definitionThumbprint = FileThumbPrint.Create(tagHelperOutputCache); - - // Act - 2 - var page = Path.Combine(projectDirectory.Path, "Views", "Shared", "NavMenu.razor"); - File.WriteAllText(page, updatedContent); - - build = new BuildCommand(projectDirectory); - result = build.Execute(); - - // Assert - 2 - new FileInfo(outputFile).Should().Exist(); - Assert.NotEqual(outputAssemblyThumbprint, FileThumbPrint.Create(outputFile)); - - new FileInfo(generatedDefinitionFile).Should().Exist(); - Assert.NotEqual(generatedDefinitionThumbprint, FileThumbPrint.Create(generatedDefinitionFile)); - new FileInfo(generatedFile).Should().Exist(); - Assert.NotEqual(generatedFileThumbprint, FileThumbPrint.Create(generatedFile)); - - new FileInfo(tagHelperOutputCache).Should().Exist(); - new FileInfo(tagHelperOutputCache).Should().Contain(@"""Name"":""MvcWithComponents.Views.Shared.NavMenu"""); - - new FileInfo(tagHelperOutputCache).Should().Contain("AParameter"); - - Assert.NotEqual(definitionThumbprint, FileThumbPrint.Create(tagHelperOutputCache)); - } - - [Fact] - public void BuildComponents_DoesNotModifyFiles_IfFilesDoNotChange() - { - var testAsset = "RazorMvcWithComponents"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - var build = new BuildCommand(projectDirectory); - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - // Act - 1 - var tagHelperOutputCache = Path.Combine(intermediateOutputPath, "MvcWithComponents.TagHelpers.output.cache"); - - var file = Path.Combine(projectDirectory.Path, "Views", "Shared", "NavMenu.razor.g.cs"); - var generatedFile = Path.Combine(intermediateOutputPath, "Razor", "Views", "Shared", "NavMenu.razor.g.cs"); - var generatedDefinitionFile = Path.Combine(intermediateOutputPath, "RazorDeclaration", "Views", "Shared", "NavMenu.razor.g.cs"); - - // Assert - 1 - var result = build.Execute(); - - result.Should().Pass(); - var outputFile = Path.Combine(outputPath, "MvcWithComponents.dll"); - new FileInfo(outputFile).Should().Exist(); - var outputAssemblyThumbprint = FileThumbPrint.Create(outputFile); - - new FileInfo(generatedDefinitionFile).Should().Exist(); - var generatedDefinitionThumbprint = FileThumbPrint.Create(generatedDefinitionFile); - new FileInfo(generatedFile).Should().Exist(); - var generatedFileThumbprint = FileThumbPrint.Create(generatedFile); - - new FileInfo(tagHelperOutputCache).Should().Exist(); - new FileInfo(tagHelperOutputCache).Should().Contain(@"""Name"":""MvcWithComponents.Views.Shared.NavMenu"""); - - var definitionThumbprint = FileThumbPrint.Create(tagHelperOutputCache); - - // Act - 2 - result = build.Execute(); - - // Assert - 2 - new FileInfo(outputFile).Should().Exist(); - Assert.Equal(outputAssemblyThumbprint, FileThumbPrint.Create(outputFile)); - - new FileInfo(generatedDefinitionFile).Should().Exist(); - Assert.Equal(generatedDefinitionThumbprint, FileThumbPrint.Create(generatedDefinitionFile)); - new FileInfo(generatedFile).Should().Exist(); - Assert.Equal(generatedFileThumbprint, FileThumbPrint.Create(generatedFile)); - - new FileInfo(tagHelperOutputCache).Should().Exist(); - new FileInfo(tagHelperOutputCache).Should().Contain(@"""Name"":""MvcWithComponents.Views.Shared.NavMenu"""); - - Assert.Equal(definitionThumbprint, FileThumbPrint.Create(tagHelperOutputCache)); - } - [Fact] public void IncrementalBuild_WithP2P_WorksWhenBuildProjectReferencesIsDisabled() { @@ -386,7 +237,7 @@ public void IncrementalBuild_WithP2P_WorksWhenBuildProjectReferencesIsDisabled() new FileInfo(Path.Combine(outputPath, "ClassLibrary.Views.pdb")).Should().Exist(); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/28780")] public void Build_TouchesUpToDateMarkerFile() { var testAsset = "RazorClassLibrary"; diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIntegrationTest.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIntegrationTest.cs index 2f3661e4a8e5..5b2af49bbf95 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIntegrationTest.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/BuildIntegrationTest.cs @@ -39,7 +39,7 @@ private void Build_SimpleMvc_WithoutBuildServer_CanBuildSuccessfully() var build = new BuildCommand(projectDirectory); var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - build.Execute("/p:UseRazorBuildServer=false") + build.Execute("/p:_RazorSourceGeneratorWriteGeneratedOutput=true") .Should() .Pass() .And.HaveStdOutContaining($"SimpleMvc -> {Path.Combine(projectDirectory.Path, outputPath, "SimpleMvc.Views.dll")}"); diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/DesignTimeBuildIntegrationTest.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/DesignTimeBuildIntegrationTest.cs index d96e33fcecf2..323a108e536c 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/DesignTimeBuildIntegrationTest.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/DesignTimeBuildIntegrationTest.cs @@ -42,7 +42,7 @@ public void DesignTimeBuild_DoesNotRunRazorTargets() new FileInfo(Path.Combine(outputPath, "SimpleMvc.Views.pdb")).Should().NotExist(); } - [Fact] + [Fact(Skip = "Skipping until https://github.com/dotnet/aspnetcore/issues/28825 is resolved.")] public void RazorGenerateDesignTime_ReturnsRazorGenerateWithTargetPath() { var testAsset = "RazorSimpleMvc"; diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTest50.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTest50.cs index e29420429a09..82c551b1e930 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTest50.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTest50.cs @@ -18,6 +18,6 @@ public class MvcBuildIntegrationTest50 : MvcBuildIntegrationTestLegacy public MvcBuildIntegrationTest50(ITestOutputHelper log) : base(log) {} public override string TestProjectName => "SimpleMvc50"; - public override string TargetFramework => DefaultTfm; + public override string TargetFramework => "net5.0"; } } diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTestLegacy.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTestLegacy.cs index e4c3c92c9620..a1f535f09a5f 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTestLegacy.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/MvcBuildIntegrationTestLegacy.cs @@ -49,7 +49,6 @@ public virtual void Building_Project() new FileInfo( Path.Combine(intermediateOutputPath, $"{TestProjectName}.TagHelpers.output.cache")).Should().Contain( @"""Name"":""SimpleMvc.SimpleTagHelper"""); - } [CoreMSBuildOnlyFact] diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/PublishIntegrationTest.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/PublishIntegrationTest.cs index 04eaefeb2c46..b3cd1dfd1ea6 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/PublishIntegrationTest.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/PublishIntegrationTest.cs @@ -55,7 +55,7 @@ public void Publish_RazorCompileOnPublish_IsDefault() new DirectoryInfo(Path.Combine(publishOutputPath, "Views")).Should().NotExist(); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/28781")] public void Publish_WithRazorCompileOnBuildFalse_PublishesAssembly() { var testAsset = "RazorSimpleMvc"; diff --git a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/ScopedCssIntegrationTests.cs b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/ScopedCssIntegrationTests.cs index 3ca088b4b0b9..4f3f76d66c52 100644 --- a/src/Tests/Microsoft.NET.Sdk.Razor.Tests/ScopedCssIntegrationTests.cs +++ b/src/Tests/Microsoft.NET.Sdk.Razor.Tests/ScopedCssIntegrationTests.cs @@ -78,7 +78,7 @@ public void CanOverrideScopeIdentifiers() File.Move(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"), styles); var build = new BuildCommand(projectDirectory); - build.Execute("/p:EnableDefaultScopedCssItems=false").Should().Pass(); + build.Execute("/p:EnableDefaultScopedCssItems=false", "/p:_RazorSourceGeneratorWriteGeneratedOutput=true").Should().Pass(); var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); @@ -215,7 +215,7 @@ public void Build_GeneratedComponentContainsScope() var projectDirectory = CreateAspNetSdkTestAsset(testAsset); var build = new BuildCommand(projectDirectory); - build.Execute().Should().Pass(); + build.Execute("/p:_RazorSourceGeneratorWriteGeneratedOutput=true").Should().Pass(); var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); @@ -239,7 +239,7 @@ public void Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle() var projectDirectory = CreateAspNetSdkTestAsset(testAsset); var build = new BuildCommand(projectDirectory); - build.Execute().Should().Pass(); + build.Execute("/p:_RazorSourceGeneratorWriteGeneratedOutput=true").Should().Pass(); var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); @@ -257,7 +257,7 @@ public void Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle() File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); build = new BuildCommand(projectDirectory); - build.Execute().Should().Pass(); + build.Execute("/p:_RazorSourceGeneratorWriteGeneratedOutput=true").Should().Pass(); new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); new FileInfo(generatedCounter).Should().Exist(); @@ -299,7 +299,7 @@ public void Build_ScopedCssTransformation_AndBundling_IsIncremental() // Act & Assert 1 var build = new BuildCommand(projectDirectory); - build.Execute().Should().Pass(); + build.Execute("/p:_RazorSourceGeneratorWriteGeneratedOutput=true").Should().Pass(); var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); var directoryPath = Path.Combine(intermediateOutputPath, "scopedcss");