From 35228fd31ed6792a12cf5d3951c044a192221c5d Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 24 Dec 2025 15:35:44 +0100 Subject: [PATCH] Fix ScopedCss incrementalism to detect CssScope metadata changes When users specify a custom CssScope metadata on their scoped CSS files, the build system should regenerate the scoped CSS output when that value changes. Previously, MSBuild's Inputs/Outputs incrementalism only tracked file timestamps, not item metadata like CssScope. This fix: - Adds a _CreateScopedCssScopeCache target that writes CssScope values to a cache file using WriteLinesToFile with WriteOnlyWhenDifferent - Includes the cache file in _ProcessScopedCssFiles target Inputs - Sets SkipIfOutputIsNewer=false on RewriteCss since target-level incrementalism now handles the decision to run Fixes #50646 --- ....NET.Sdk.StaticWebAssets.ScopedCss.targets | 33 ++++++- .../ScopedCssIntegrationTests.cs | 94 +++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets index e74f0589b497..e04fa4a14ec3 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets @@ -142,6 +142,33 @@ Integration with static web assets: + + + + <_ScopedCssIntermediatePath>$([System.IO.Path]::GetFullPath($(IntermediateOutputPath)scopedcss\)) + <_ScopedCssScopeCacheFile>$(_ScopedCssIntermediatePath)scopedcss.cache + + + + + + + + + + + + @@ -171,10 +198,12 @@ Integration with static web assets: BeforeTargets="CollectUpToDateCheckInputDesignTime;CollectUpToDateCheckBuiltDesignTime" /> - + - + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs index 6155a3498987..26b7db5eb7a8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs @@ -395,6 +395,100 @@ public void Build_ScopedCssTransformation_AndBundling_IsIncremental() } } + // Regression test for https://github.com/dotnet/sdk/issues/50646 + [Fact] + public void Build_RegeneratesScopedCss_WhenCssScopeMetadataChanges() + { + // Arrange + var testAsset = "RazorComponentApp"; + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + // Act 1: First build without custom scope + var build = CreateBuildCommand(projectDirectory); + ExecuteCommand(build).Should().Pass(); + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + var scopedCssFile = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + var bundleFile = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); + + new FileInfo(scopedCssFile).Should().Exist(); + new FileInfo(bundleFile).Should().Exist(); + + // Get initial thumbprints + var initialScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + var initialBundleThumbprint = FileThumbPrint.Create(bundleFile); + + // Verify initial build uses auto-generated scope (starts with 'b-') + var initialContent = File.ReadAllText(scopedCssFile); + initialContent.Should().MatchRegex(@"\[b-[a-z0-9]+\]"); + + // Act 2: Add custom CssScope metadata to the project + File.WriteAllText( + Path.Combine(projectDirectory.Path, "Directory.Build.targets"), + """ + + + + my-custom-scope + + + + """); + + build = CreateBuildCommand(projectDirectory); + ExecuteCommand(build).Should().Pass(); + + // Assert: Files should be regenerated with the new scope + var newScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + var newBundleThumbprint = FileThumbPrint.Create(bundleFile); + + Assert.NotEqual(initialScopedCssThumbprint, newScopedCssThumbprint); + Assert.NotEqual(initialBundleThumbprint, newBundleThumbprint); + + // Verify the new content uses the custom scope + var newContent = File.ReadAllText(scopedCssFile); + newContent.Should().Contain("[my-custom-scope]"); + newContent.Should().NotMatchRegex(@"\[b-[a-z0-9]+\]"); + + // Act 3: Change the custom scope to a different value + File.WriteAllText( + Path.Combine(projectDirectory.Path, "Directory.Build.targets"), + """ + + + + my-updated-scope + + + + """); + + build = CreateBuildCommand(projectDirectory); + ExecuteCommand(build).Should().Pass(); + + // Assert: Files should be regenerated again with the updated scope + var updatedScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + var updatedBundleThumbprint = FileThumbPrint.Create(bundleFile); + + Assert.NotEqual(newScopedCssThumbprint, updatedScopedCssThumbprint); + Assert.NotEqual(newBundleThumbprint, updatedBundleThumbprint); + + // Verify the content uses the updated scope + var updatedContent = File.ReadAllText(scopedCssFile); + updatedContent.Should().Contain("[my-updated-scope]"); + updatedContent.Should().NotContain("[my-custom-scope]"); + + // Act 4: Verify that building again without changes doesn't regenerate + var finalScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + var finalBundleThumbprint = FileThumbPrint.Create(bundleFile); + + build = CreateBuildCommand(projectDirectory); + ExecuteCommand(build).Should().Pass(); + + Assert.Equal(finalScopedCssThumbprint, FileThumbPrint.Create(scopedCssFile)); + Assert.Equal(finalBundleThumbprint, FileThumbPrint.Create(bundleFile)); + } + // This test verifies if the targets that VS calls to update scoped css works to update these files [Fact] public void RegeneratingScopedCss_ForProject()