From 3a819929a7fb969ac260d55c003944d1d9dbac27 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 24 Jul 2020 09:02:36 +0200 Subject: [PATCH] [Blazor] Wires up CSS isolation (#24221) * Wires up CSS isolation on the build. * Transforms the css files during build. * Bundles all scopes css into a single file and exposes it on _framework/scoped.styles.cs * Packs pre-processed files as static web assets. --- .../WasmBuildIntegrationTest.cs | 3 + .../WasmPublishIntegrationTest.cs | 104 ++++++++ ....NET.Sdk.BlazorWebAssembly.Current.targets | 1 + .../src/Application.cs | 4 +- .../BuildIncrementalismTest.cs | 5 +- .../integrationtests/PackIntegrationTest.cs | 1 + .../ScopedCssIntegrationTests.cs | 204 +++++++++++++++ .../StaticWebAssetsIntegrationTest.cs | 14 +- .../src/ApplyCssScopes.cs | 95 +++++++ .../src/ComputeCssScope.cs | 71 +++++ .../src/ConcatenateCssFiles.cs | 72 ++++++ .../src/DiscoverDefaultScopedCssItems.cs | 39 +++ .../src/GenerateStaticWebAsssetsPropsFile.cs | 9 +- .../src/ResolveAllScopedCssAssets.cs | 38 +++ .../Microsoft.NET.Sdk.Razor/src/RewriteCss.cs | 59 +++++ .../Microsoft.NET.Sdk.Razor.ScopedCss.targets | 244 ++++++++++++++++++ .../Sdk.Razor.CurrentVersion.props | 5 + .../Sdk.Razor.CurrentVersion.targets | 5 + .../GenerateStaticWebAssetsPropsFileTest.cs | 4 +- .../ClassLibrary/Components/App.razor | 1 + .../ClassLibrary/Components/App.razor.css | 3 + .../Components/Pages/Counter.razor.css | 3 + .../Components/Pages/Index.razor.css | 3 + .../Components/App.razor | 1 + .../Components/App.razor.css | 3 + 25 files changed, 979 insertions(+), 12 deletions(-) create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ScopedCssIntegrationTests.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/ApplyCssScopes.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/ComputeCssScope.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/ConcatenateCssFiles.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/DiscoverDefaultScopedCssItems.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/ResolveAllScopedCssAssets.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/RewriteCss.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets create mode 100644 src/Razor/test/testassets/ClassLibrary/Components/App.razor create mode 100644 src/Razor/test/testassets/ClassLibrary/Components/App.razor.css create mode 100644 src/Razor/test/testassets/ComponentApp/Components/Pages/Counter.razor.css create mode 100644 src/Razor/test/testassets/ComponentApp/Components/Pages/Index.razor.css create mode 100644 src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor create mode 100644 src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor.css diff --git a/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs index 0f109465ad..7313c0b243 100644 --- a/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs @@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Arrange // Minimal has no project references, service worker etc. This is pretty close to the project template. using var project = ProjectDirectory.Create("blazorwasm-minimal"); + File.WriteAllText(Path.Combine(project.DirectoryPath, "App.razor.css"), "h1 { font-size: 16px; }"); + var result = await MSBuildProcessManager.DotnetMSBuild(project); Assert.BuildPassed(result); @@ -35,6 +37,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests var staticWebAssets = Assert.FileExists(result, buildOutputDirectory, "blazorwasm-minimal.StaticWebAssets.xml"); Assert.FileContains(result, staticWebAssets, Path.Combine(project.TargetFramework, "wwwroot")); + Assert.FileContains(result, staticWebAssets, Path.Combine(project.TargetFramework, "scopedcss")); } [Fact] diff --git a/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs index 590153a114..0a14040ba5 100644 --- a/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs @@ -93,6 +93,58 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests VerifyTypeGranularTrimming(result, blazorPublishDirectory); } + [Fact] + public async Task Publish_WithScopedCss_Works() + { + // Arrange + using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "LinkBaseToWebRoot" }); + File.WriteAllText(Path.Combine(project.DirectoryPath, "App.razor.css"), "h1 { font-size: 16px; }"); + + project.Configuration = "Debug"; + var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); + + Assert.BuildPassed(result); + + var publishDirectory = project.PublishOutputDirectory; + + var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); + + Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. + + // Verify scoped css + Assert.FileExists(result, blazorPublishDirectory, "_framework", "scoped.styles.css"); + + // Verify referenced static web assets + Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); + Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css"); + + // Verify static assets are in the publish directory + Assert.FileExists(result, blazorPublishDirectory, "index.html"); + + // Verify link item assets are in the publish directory + Assert.FileExists(result, blazorPublishDirectory, "js", "LinkedScript.js"); + var cssFile = Assert.FileExists(result, blazorPublishDirectory, "css", "app.css"); + Assert.FileContains(result, cssFile, ".publish"); + Assert.FileDoesNotExist(result, "dist", "Fake-License.txt"); + + // Verify web.config + Assert.FileExists(result, publishDirectory, "web.config"); + Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); + + VerifyTypeGranularTrimming(result, blazorPublishDirectory); + } + [Fact] public async Task Publish_InRelease_Works() { @@ -578,6 +630,58 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests assetsManifestPath: "custom-service-worker-assets.js"); } + [Fact] + public async Task Publish_HostedAppWithScopedCss_VisualStudio() + { + // Simulates publishing the same way VS does by setting BuildProjectReferences=false. + // Arrange + using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); + File.WriteAllText(Path.Combine(project.SolutionPath, "blazorwasm", "App.razor.css"), "h1 { font-size: 16px; }"); + + project.Configuration = "Release"; + var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", "/p:BuildInsideVisualStudio=true"); + + Assert.BuildPassed(result); + + result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false /p:BuildInsideVisualStudio=true"); + + var publishDirectory = project.PublishOutputDirectory; + // Make sure the main project exists + Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); + + var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. + + // Verify scoped css + Assert.FileExists(result, blazorPublishDirectory, "_framework", "scoped.styles.css"); + + // Verify static assets are in the publish directory + Assert.FileExists(result, blazorPublishDirectory, "index.html"); + + // Verify static web assets from referenced projects are copied. + Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); + Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); + + // Verify compression works + Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br"); + Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); + + // Verify web.config + Assert.FileExists(result, publishDirectory, "web.config"); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); + } + // Regression test to verify satellite assemblies from the blazor app are copied to the published app's wwwroot output directory as // part of publishing in VS [Fact] diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets index f0b8941d12..a0c4faa979 100644 --- a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets +++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets @@ -246,6 +246,7 @@ Copyright (c) .NET Foundation. All rights reserved. $(GetCurrentProjectStaticWebAssetsDependsOn); + AddScopedCssBundle; _BlazorWasmPrepareForRun; diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/Application.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/Application.cs index d4c57b799d..de4870f823 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/Application.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/Application.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -113,4 +113,4 @@ namespace Microsoft.AspNetCore.Razor.Tools return expandedArgs.ToArray(); } } -} \ No newline at end of file +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs index d9a4cfc8e9..df84c3b7a6 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -320,6 +320,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [InitializeTestProject("ClassLibrary")] public async Task Build_TouchesUpToDateMarkerFile() { + // Remove the components so that they don't interfere with these tests + Directory.Delete(Path.Combine(Project.DirectoryPath, "Components"), recursive: true); + var classLibraryDll = Path.Combine(IntermediateOutputPath, "ClassLibrary.dll"); var classLibraryViewsDll = Path.Combine(IntermediateOutputPath, "ClassLibrary.Views.dll"); var markerFile = Path.Combine(IntermediateOutputPath, "ClassLibrary.csproj.CopyComplete"); diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/PackIntegrationTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/PackIntegrationTest.cs index 6b0688031c..d2b2370637 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/PackIntegrationTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/PackIntegrationTest.cs @@ -246,6 +246,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests filePaths: new[] { Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + Path.Combine("staticwebassets", "Components", "App.razor.rz.scp.css"), Path.Combine("staticwebassets", "css", "site.css"), Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), Path.Combine("build", "PackageLibraryDirectDependency.props"), diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ScopedCssIntegrationTests.cs b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ScopedCssIntegrationTests.cs new file mode 100644 index 0000000000..842cf6820e --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ScopedCssIntegrationTests.cs @@ -0,0 +1,204 @@ +// 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.RegularExpressions; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests +{ + public class ScopedCssIntegrationTest : MSBuildIntegrationTestBase, IClassFixture + { + public ScopedCssIntegrationTest( + BuildServerTestFixture buildServer, + ITestOutputHelper output) + : base(buildServer) + { + Output = output; + } + + public ITestOutputHelper Output { get; private set; } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Build_GeneratesTransformedFilesAndBundle_ForComponentsWithScopedCss() + { + var result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css"); + Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css"); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Build_ScopedCssFiles_ContainsUniqueScopesPerFile() + { + var result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + var generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + var generatedIndex = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css"); + var counterContent = File.ReadAllText(generatedCounter); + var indexContent = File.ReadAllText(generatedIndex); + + var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); + Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); + var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value; + + var indexScopeMatch = Regex.Match(indexContent, ".*h1\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); + Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; + + Assert.NotEqual(counterScopeId, indexScopeId); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Publish_PublishesBundleToTheRightLocation() + { + var result = await DotnetMSBuild("Publish"); + Assert.BuildPassed(result); + + Assert.FileExists(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css"); + Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css"); + Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css"); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Publish_NoBuild_PublishesBundleToTheRightLocation() + { + var result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + result = await DotnetMSBuild("Publish", "/p:NoBuild=true"); + Assert.BuildPassed(result); + + Assert.FileExists(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css"); + Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css"); + Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css"); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Publish_DoesNotPublishAnyFile_WhenThereAreNoScopedCssFiles() + { + File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Counter.razor.css")); + File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Index.razor.css")); + + var result = await DotnetMSBuild("Publish"); + Assert.BuildPassed(result); + + Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css"); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Build_GeneratedComponentContainsScope() + { + var result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + var generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + Assert.FileExists(result, IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs"); + + var counterContent = File.ReadAllText(generatedCounter); + + var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); + Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); + var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value; + + Assert.FileContains(result, Path.Combine(IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs"), counterScopeId); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle() + { + var result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + var generatedBundle = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css"); + var generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs"); + + var componentThumbprint = GetThumbPrint(generatedCounter); + var bundleThumbprint = GetThumbPrint(generatedBundle); + + File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Counter.razor.css")); + + result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs"); + + var newComponentThumbprint = GetThumbPrint(generatedCounter); + var newBundleThumbprint = GetThumbPrint(generatedBundle); + + Assert.NotEqual(componentThumbprint, newComponentThumbprint); + Assert.NotEqual(bundleThumbprint, newBundleThumbprint); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Does_Nothing_WhenThereAreNoScopedCssFiles() + { + File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Counter.razor.css")); + File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Index.razor.css")); + + var result = await DotnetMSBuild("Build"); + Assert.BuildPassed(result); + + Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css"); + } + + [Fact] + [InitializeTestProject("ComponentApp", language: "C#")] + public async Task Build_ScopedCssTransformation_AndBundling_IsIncremental() + { + // Arrange + var thumbprintLookup = new Dictionary(); + + // Act 1 + var result = await DotnetMSBuild("Build"); + + var directoryPath = Path.Combine(result.Project.DirectoryPath, IntermediateOutputPath, "scopedcss"); + + var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); + foreach (var file in files) + { + var thumbprint = GetThumbPrint(file); + thumbprintLookup[file] = thumbprint; + } + + // Assert 1 + Assert.BuildPassed(result); + + // Act & Assert 2 + for (var i = 0; i < 2; i++) + { + // We want to make sure nothing changed between multiple incremental builds. + using (var razorGenDirectoryLock = LockDirectory(RazorIntermediateOutputPath)) + { + result = await DotnetMSBuild("Build"); + } + + Assert.BuildPassed(result); + foreach (var file in files) + { + var thumbprint = GetThumbPrint(file); + Assert.Equal(thumbprintLookup[file], thumbprint); + } + } + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/StaticWebAssetsIntegrationTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/StaticWebAssetsIntegrationTest.cs index 63c0afee0f..3d9d7149e8 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/StaticWebAssetsIntegrationTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/StaticWebAssetsIntegrationTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -72,6 +72,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(result, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "css", "site.css")); Assert.FileExists(result, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "js", "pkg-direct-dep.js")); Assert.FileExists(result, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); + Assert.FileExists(result, PublishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReference", "_framework", "scoped.styles.css")); // Validate that static web assets don't get published as content too on their regular path Assert.FileDoesNotExist(result, PublishOutputPath, Path.Combine("wwwroot", "js", "project-transitive-dep.js")); @@ -98,6 +99,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "css", "site.css")); Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "js", "pkg-direct-dep.js")); Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReferenceAndRID", "_framework", "scoped.styles.css")); // Validate that static web assets don't get published as content too on their regular path Assert.FileDoesNotExist(result, publishOutputPath, Path.Combine("wwwroot", "js", "project-transitive-dep.js")); @@ -127,6 +129,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "css", "site.css")); Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "js", "pkg-direct-dep.js")); Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReference", "_framework", "scoped.styles.css")); } [Fact] @@ -149,6 +152,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "css", "site.css")); Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "js", "pkg-direct-dep.js")); Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReference", "_framework", "scoped.styles.css")); } [Fact] @@ -295,13 +299,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { Path.Combine(restorePath, "packagelibrarytransitivedependency", "1.0.0", "build", "..", "staticwebassets") + Path.DirectorySeparatorChar, Path.Combine(restorePath, "packagelibrarydirectdependency", "1.0.0", "build", "..", "staticwebassets") + Path.DirectorySeparatorChar, + Path.GetFullPath(Path.Combine(source, "ClassLibrary2", "wwwroot")) + Path.DirectorySeparatorChar, Path.GetFullPath(Path.Combine(source, "ClassLibrary", "wwwroot")) + Path.DirectorySeparatorChar, - Path.GetFullPath(Path.Combine(source, "ClassLibrary2", "wwwroot")) + Path.DirectorySeparatorChar + Path.GetFullPath(Path.Combine(source, "AppWithPackageAndP2PReference", IntermediateOutputPath, "scopedcss")) + Path.DirectorySeparatorChar, }; return $@" - - + + + "; diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/ApplyCssScopes.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/ApplyCssScopes.cs new file mode 100644 index 0000000000..d38ba4d465 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/ApplyCssScopes.cs @@ -0,0 +1,95 @@ +// 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.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class ApplyCssScopes : Task + { + [Required] + public ITaskItem[] RazorComponents { get; set; } + + [Required] + public ITaskItem[] ScopedCss { get; set; } + + [Output] + public ITaskItem[] RazorComponentsWithScopes { get; set; } + + public override bool Execute() + { + var razorComponentsWithScopes = new List(); + var unmatchedScopedCss = new List(ScopedCss); + var scopedCssByComponent = new Dictionary>(); + + for (var i = 0; i < RazorComponents.Length; i++) + { + var componentCandidate = RazorComponents[i]; + var j = 0; + while (j < unmatchedScopedCss.Count) + { + var scopedCssCandidate = unmatchedScopedCss[j]; + var explicitRazorcomponent = scopedCssCandidate.GetMetadata("RazorComponent"); + var razorComponent = !string.IsNullOrWhiteSpace(explicitRazorcomponent) ? + explicitRazorcomponent : + Regex.Replace(scopedCssCandidate.ItemSpec, "(.*)\\.razor\\.css$", "$1.razor", RegexOptions.IgnoreCase); + + if (string.Equals(componentCandidate.ItemSpec, razorComponent, StringComparison.OrdinalIgnoreCase)) + { + unmatchedScopedCss.RemoveAt(j); + if (!scopedCssByComponent.TryGetValue(componentCandidate.ItemSpec, out var existing)) + { + scopedCssByComponent[componentCandidate.ItemSpec] = new List() { scopedCssCandidate }; + var item = new TaskItem(componentCandidate); + item.SetMetadata("CssScope", scopedCssCandidate.GetMetadata("CssScope")); + razorComponentsWithScopes.Add(item); + } + else + { + existing.Add(scopedCssCandidate); + } + } + else + { + j++; + } + } + } + + foreach (var kvp in scopedCssByComponent) + { + var component = kvp.Key; + var scopeFiles = kvp.Value; + + if (scopeFiles.Count > 1) + { + Log.LogError($"More than one scoped css files were found for the razor component '{component}'. Each razor component must have at most" + + " a single associated scoped css file." + Environment.NewLine + string.Join(Environment.NewLine, scopeFiles.Select(f => f.ItemSpec))); + } + } + + // We don't want to allow scoped css files without a matching component. Our convention is very specific in its requirements + // so failing to have a matching component very likely means an error. + // When the matching component was specified explicitly, failing to find a matching component is an error. + // This simplifies a few things like being able to assume that the presence of a .razor.css file or a ScopedCssInput item will result in a bundle being produced, + // that the contents of the bundle are independent of the existence of a component and that users will be able to catch errors at compile + // time instead of wondering why their component doesn't have a scope applied to it. + // In the rare case that a .razor file exists on the user project, has an associated .razor.css file and the user decides to exclude it as a RazorComponent they + // can update the Content item for the .razor.css file with Scoped=false and we will not consider it. + foreach (var unmatched in unmatchedScopedCss) + { + Log.LogError($"The scoped css file '{unmatched.ItemSpec}' was defined but no associated razor component was found for it."); + } + + RazorComponentsWithScopes = razorComponentsWithScopes.ToArray(); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/ComputeCssScope.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/ComputeCssScope.cs new file mode 100644 index 0000000000..bd5b974289 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/ComputeCssScope.cs @@ -0,0 +1,71 @@ +// 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.Numerics; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class ComputeCssScope : Task + { + [Required] + public ITaskItem[] ScopedCssInput { get; set; } + + [Required] + public string TargetName { get; set; } + + [Output] + public ITaskItem[] ScopedCss { get; set; } + + public override bool Execute() + { + ScopedCss = new ITaskItem[ScopedCssInput.Length]; + + for (var i = 0; i < ScopedCssInput.Length; i++) + { + var input = ScopedCssInput[i]; + // Todo: Normalize path to forward slashes and lowercase before computing the hash + var relativePath = input.ItemSpec.Replace("\\","//"); + var scope = input.GetMetadata("CssScope"); + scope = !string.IsNullOrEmpty(scope) ? scope : GenerateScope(TargetName, relativePath); + + var outputItem = new TaskItem(input); + outputItem.SetMetadata("CssScope", scope); + ScopedCss[i] = outputItem; + } + + return !Log.HasLoggedErrors; + } + + private string GenerateScope(string targetName, string relativePath) + { + using var hash = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(relativePath + targetName); + var hashBytes = hash.ComputeHash(bytes); + + var builder = new StringBuilder(); + builder.Append("b-"); + + builder.Append(ToBase36(hashBytes)); + + return builder.ToString(); + } + + private string ToBase36(byte[] hash) + { + var builder = new StringBuilder(); + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + var dividend = new BigInteger(hash.AsSpan().Slice(0,8).ToArray()); + while (dividend > 36) + { + dividend = BigInteger.DivRem(dividend, 36, out var remainder); + builder.Insert(0, chars[Math.Abs(((int)remainder))]); + } + return builder.ToString(); + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/ConcatenateCssFiles.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/ConcatenateCssFiles.cs new file mode 100644 index 0000000000..9993d4dd25 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/ConcatenateCssFiles.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class ConcatenateCssFiles : Task + { + [Required] + public ITaskItem[] FilesToProcess { get; set; } + + [Required] + public string OutputFile { get; set; } + + public override bool Execute() + { + var builder = new StringBuilder(); + var orderedFiles = FilesToProcess.OrderBy(f => f.GetMetadata("FullPath")).ToArray(); + for (var i = 0; i < orderedFiles.Length; i++) + { + var current = orderedFiles[i]; + builder.AppendLine($"/* {current.GetMetadata("BasePath").Replace("\\","/")}{current.GetMetadata("RelativePath").Replace("\\","/")} */"); + foreach (var line in File.ReadLines(FilesToProcess[i].GetMetadata("FullPath"))) + { + builder.AppendLine(line); + } + } + + var content = builder.ToString(); + + if (!File.Exists(OutputFile) || !SameContent(content, OutputFile)) + { + Directory.CreateDirectory(Path.GetDirectoryName(OutputFile)); + File.WriteAllText(OutputFile, content); + } + + + return !Log.HasLoggedErrors; + } + + private bool SameContent(string content, string outputFilePath) + { + var contentHash = GetContentHash(content); + + var outputContent = File.ReadAllText(outputFilePath); + var outputContentHash = GetContentHash(outputContent); + + for (int i = 0; i < outputContentHash.Length; i++) + { + if (outputContentHash[i] != contentHash[i]) + { + return false; + } + } + + return true; + + static byte[] GetContentHash(string content) + { + using var sha256 = SHA256.Create(); + return sha256.ComputeHash(Encoding.UTF8.GetBytes(content)); + } + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/DiscoverDefaultScopedCssItems.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/DiscoverDefaultScopedCssItems.cs new file mode 100644 index 0000000000..7d88ae0d3b --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/DiscoverDefaultScopedCssItems.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; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class DiscoverDefaultScopedCssItems : Task + { + [Required] + public ITaskItem[] Content { get; set; } + + [Output] + public ITaskItem[] DiscoveredScopedCssInputs { get; set; } + + public override bool Execute() + { + var discoveredInputs = new List(); + + for (var i = 0; i < Content.Length; i++) + { + var candidate = Content[i]; + var fullPath = candidate.GetMetadata("FullPath"); + if (fullPath.EndsWith(".razor.css", StringComparison.OrdinalIgnoreCase) && + !string.Equals("false", candidate.GetMetadata("Scoped"), StringComparison.OrdinalIgnoreCase)) + { + discoveredInputs.Add(candidate); + } + } + + DiscoveredScopedCssInputs = discoveredInputs.ToArray(); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAsssetsPropsFile.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAsssetsPropsFile.cs index 4146eed9fa..19637ec8e5 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAsssetsPropsFile.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAsssetsPropsFile.cs @@ -1,4 +1,4 @@ -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; @@ -106,7 +106,10 @@ namespace Microsoft.AspNetCore.Razor.Tasks if (!ValidateMetadataMatches(firstAsset, webAsset, SourceId) || !ValidateMetadataMatches(firstAsset, webAsset, SourceType) || - !ValidateMetadataMatches(firstAsset, webAsset, ContentRoot) || + // Now that we support generated assets we need to be able to support multiple content roots. + // We need to change this check for one that ensures that no two files end up in the same final destination + //!ValidateMetadataMatches(firstAsset, webAsset, ContentRoot) || + // See https://github.com/dotnet/aspnetcore/issues/24257 !ValidateMetadataMatches(firstAsset, webAsset, BasePath)) { return false; @@ -156,4 +159,4 @@ namespace Microsoft.AspNetCore.Razor.Tasks return false; } } -} \ No newline at end of file +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/ResolveAllScopedCssAssets.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/ResolveAllScopedCssAssets.cs new file mode 100644 index 0000000000..eaad897da2 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/ResolveAllScopedCssAssets.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class ResolveAllScopedCssAssets : Task + { + [Required] + public ITaskItem[] StaticWebAssets { get; set; } + + [Output] + public ITaskItem[] ScopedCssAssets { get; set; } + + public override bool Execute() + { + var scopedCssAssets = new List(); + + for (var i = 0; i < StaticWebAssets.Length; i++) + { + var swa = StaticWebAssets[i]; + var fullPath = swa.GetMetadata("RelativePath"); + if (fullPath.EndsWith(".rz.scp.css", StringComparison.OrdinalIgnoreCase)) + { + scopedCssAssets.Add(swa); + } + } + + ScopedCssAssets = scopedCssAssets.ToArray(); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/RewriteCss.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/RewriteCss.cs new file mode 100644 index 0000000000..dc79125790 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/RewriteCss.cs @@ -0,0 +1,59 @@ +// 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 System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class RewriteCss : DotNetToolTask + { + [Required] + public ITaskItem[] FilesToTransform { get; set; } + + public bool SkipIfOutputIsNewer { get; set; } = true; + + internal override string Command => "rewritecss"; + + protected override string GenerateResponseFileCommands() + { + var builder = new StringBuilder(); + + builder.AppendLine(Command); + + for (var i = 0; i < FilesToTransform.Length; i++) + { + var input = FilesToTransform[i]; + var inputFullPath = input.GetMetadata("FullPath"); + var relativePath = input.GetMetadata("RelativePath"); + var cssScope = input.GetMetadata("CssScope"); + var outputPath = input.GetMetadata("OutputFile"); + + if (SkipIfOutputIsNewer && File.Exists(outputPath) && File.GetLastWriteTimeUtc(inputFullPath) < File.GetLastWriteTimeUtc(outputPath)) + { + Log.LogMessage(MessageImportance.Low, $"Skipping scope transformation for '{input.ItemSpec}' because '{outputPath}' is newer than '{input.ItemSpec}'."); + continue; + } + + builder.AppendLine("-s"); + builder.AppendLine(inputFullPath); + + builder.AppendLine("-o"); + builder.AppendLine(outputPath); + + // Create the directory for the output file in case it doesn't exist. + // Its easier to do it here than on MSBuild. Alternatively the tool could have taken care of it. + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + builder.AppendLine("-c"); + builder.AppendLine(cssScope); + } + + return builder.ToString(); + } + + internal static string CalculateTargetPath(string relativePath, string extension) => + Path.ChangeExtension(relativePath, $"{extension}{Path.GetExtension(relativePath)}"); + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets new file mode 100644 index 0000000000..5a20c17ad1 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + <_ScopedCssExtension>.rz.scp.css + $(ResolveStaticWebAssetsInputsDependsOn);_CollectAllScopedCssAssets;AddScopedCssBundle + $(ResolveCurrentProjectStaticWebAssetsInputsDependsOn);_AddGeneratedScopedCssFiles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ScopedCssIntermediatePath>$([System.IO.Path]::GetFullPath($(IntermediateOutputPath)scopedcss\)) + + + + <_ScopedCss Condition="'%(_ScopedCss.Identity)' != ''"> + $(_ScopedCssIntermediatePath)%(RelativeDir)%(RecursiveDir)%(FileName)$(_ScopedCssExtension) + + <_ScopedCssOutputs Include="%(_ScopedCss.OutputFile)" /> + + + + + + + + + + + + + + + + + + + <_ScopedCssOutputPath>$(_ScopedCssIntermediatePath)_framework\scoped.styles.css + <_ScopedCssOutputFullPath>$([System.IO.Path]::Combine('$(MSBuildProjectFileDirectory)', '$(_ScopedCssIntermediatePath)_framework\scoped.styles.css')) + + + + + + + + $(PackageId) + $(_ScopedCssIntermediatePath) + $(StaticWebAssetBasePath) + _framework/scoped.styles.css + + <_ExternalStaticWebAsset Include="$(_ScopedCssOutputPath)" Condition="@(_AllScopedCss) != ''"> + generated + $(PackageId) + $(_ScopedCssIntermediatePath) + $(StaticWebAssetBasePath) + _framework/scoped.styles.css + + + + + + + + + + + + + + + + + _content/$(PackageId) + + + + + $(PackageId) + $(IntermediateOutputPath)scopedcss\ + $(StaticWebAssetBasePath) + $([MSBuild]::MakeRelative('$(_ScopedCssIntermediatePath)','%(_ScopedCss.OutputFile)')) + + + + + + + + + + + + + + + + + + + + <_ExternalPublishStaticWebAsset Include="$(_ScopedCssOutputFullPath)" ExcludeFromSingleFile="true"> + generated + $(PackageId) + $(_ScopedCssIntermediatePath) + $(StaticWebAssetBasePath) + PreserveNewest + $([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)',$([MSBuild]::NormalizePath('wwwroot/$(StaticWebAssetBasePath)/_framework/scoped.styles.css')))) + + + + + + + <_CurrentProjectStaticWebAsset Remove="$(_ScopedCssOutputFullPath)" /> + + + + + + diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.props b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.props index 19e380c3a4..3bc7a24d9e 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.props +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.props @@ -36,6 +36,11 @@ Copyright (c) .NET Foundation. All rights reserved. --> true + + true + $(_Targeting30OrNewerRazorLangVersion) + + + $(_Targeting30OrNewerRazorLangVersion) @@ -353,6 +356,8 @@ Copyright (c) .NET Foundation. All rights reserved. + + diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateStaticWebAssetsPropsFileTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateStaticWebAssetsPropsFileTest.cs index 2525e15626..48a70dca61 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateStaticWebAssetsPropsFileTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateStaticWebAssetsPropsFileTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -269,7 +269,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks Assert.Equal(expectedError, message); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/24257")] public void Fails_WhenStaticWebAsset_HaveDifferentContentRoot() { // Arrange diff --git a/src/Razor/test/testassets/ClassLibrary/Components/App.razor b/src/Razor/test/testassets/ClassLibrary/Components/App.razor new file mode 100644 index 0000000000..6ab209770e --- /dev/null +++ b/src/Razor/test/testassets/ClassLibrary/Components/App.razor @@ -0,0 +1 @@ +

Hello from razor

diff --git a/src/Razor/test/testassets/ClassLibrary/Components/App.razor.css b/src/Razor/test/testassets/ClassLibrary/Components/App.razor.css new file mode 100644 index 0000000000..8bf950df47 --- /dev/null +++ b/src/Razor/test/testassets/ClassLibrary/Components/App.razor.css @@ -0,0 +1,3 @@ +p { + font-size: bold; +} diff --git a/src/Razor/test/testassets/ComponentApp/Components/Pages/Counter.razor.css b/src/Razor/test/testassets/ComponentApp/Components/Pages/Counter.razor.css new file mode 100644 index 0000000000..6fd5c7d6df --- /dev/null +++ b/src/Razor/test/testassets/ComponentApp/Components/Pages/Counter.razor.css @@ -0,0 +1,3 @@ +button { + font-size: 16px; +} diff --git a/src/Razor/test/testassets/ComponentApp/Components/Pages/Index.razor.css b/src/Razor/test/testassets/ComponentApp/Components/Pages/Index.razor.css new file mode 100644 index 0000000000..ae982b6049 --- /dev/null +++ b/src/Razor/test/testassets/ComponentApp/Components/Pages/Index.razor.css @@ -0,0 +1,3 @@ +h1 { + font-weight: bold; +} diff --git a/src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor b/src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor new file mode 100644 index 0000000000..6ab209770e --- /dev/null +++ b/src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor @@ -0,0 +1 @@ +

Hello from razor

diff --git a/src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor.css b/src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor.css new file mode 100644 index 0000000000..8bf950df47 --- /dev/null +++ b/src/Razor/test/testassets/PackageLibraryDirectDependency/Components/App.razor.css @@ -0,0 +1,3 @@ +p { + font-size: bold; +}