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;
+}