diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets index 1042c438908f..4bd2e53ba420 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets @@ -161,11 +161,12 @@ Copyright (c) .NET Foundation. All rights reserved. - $(BuildCompressionFormats);gzip - $(PublishCompressionFormats);gzip;brotli + true + $(BuildCompressionFormats);gzip + $(PublishCompressionFormats);gzip;brotli false - $(CompressionIncludePatterns) - $(CompressionExcludePatterns) + + $(_BlazorBrotliCompressionLevel) @@ -195,7 +196,7 @@ Copyright (c) .NET Foundation. All rights reserved. GeneratePublishCompressedStaticWebAssets ResolvePublishCompressedStaticWebAssetsConfiguration --> - + ResolvePublishCompressedStaticWebAssets; $(ResolvePublishRelatedStaticWebAssetsDependsOn) @@ -216,7 +217,7 @@ Copyright (c) .NET Foundation. All rights reserved. <_DotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe - + diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets index c922d5b571c8..fd3f1f68c2bd 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets @@ -660,7 +660,7 @@ Copyright (c) .NET Foundation. All rights reserved. DependsOnTargets="ResolveStaticWebAssetsConfiguration;UpdateExistingPackageStaticWebAssets"> a.Identity); + var assetsById = new Dictionary(CandidateAssets.Length, OSPath.PathComparer); - var endpointsByAsset = CandidateEndpoints.Select(StaticWebAssetEndpoint.FromTaskItem) - .GroupBy(e => e.AssetFile) - .ToDictionary(g => g.Key, g => g.ToList()); + // A good rule of thumb is that the number of compressed assets is half the number of assets. + var compressedAssets = new List(CandidateAssets.Length / 2); - var compressedAssets = assetsById.Values.Where(a => a.AssetTraitName == "Content-Encoding").ToList(); - var updatedEndpoints = new HashSet(StaticWebAssetEndpoint.RouteAndAssetComparer); + for (var i = 0; i < CandidateAssets.Length; i++) + { + var candidate = StaticWebAsset.FromTaskItem(CandidateAssets[i]); + if (assetsById.ContainsKey(CandidateAssets[i].ItemSpec)) + { + Log.LogWarning("Detected duplicated asset '{0}'. Skipping the asset because it was already processed.", candidate.Identity); + continue; + } + + assetsById[candidate.Identity] = candidate; + if (string.Equals(candidate.AssetTraitName, "Content-Encoding", StringComparison.Ordinal)) + { + compressedAssets.Add(candidate); + } + } + var endpointsByAsset = new Dictionary>(CandidateEndpoints.Length, StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < CandidateEndpoints.Length; i++) + { + var endpoint = StaticWebAssetEndpoint.FromTaskItem(CandidateEndpoints[i]); + if (!endpointsByAsset.TryGetValue(endpoint.AssetFile, out var endpoints)) + { + endpoints = []; + endpointsByAsset[endpoint.AssetFile] = endpoints; + } + endpoints.Add(endpoint); + } + + var updatedEndpoints = new HashSet(StaticWebAssetEndpoint.RouteAndAssetComparer); var preservedEndpoints = new Dictionary<(string, string), StaticWebAssetEndpoint>(); // Add response headers to compressed endpoints @@ -39,20 +64,20 @@ public override bool Execute() { if (!assetsById.TryGetValue(compressedAsset.RelatedAsset, out var relatedAsset)) { - Log.LogWarning("Related asset not found for compressed asset: {0}", compressedAsset.Identity); - throw new InvalidOperationException($"Related asset not found for compressed asset: {compressedAsset.Identity}"); + Log.LogWarning("Related asset '{0}' not found for compressed asset: '{1}'. Skipping asset", compressedAsset.RelatedAsset, compressedAsset.Identity); + continue; } if (!endpointsByAsset.TryGetValue(compressedAsset.Identity, out var compressedEndpoints)) { - Log.LogWarning("Endpoints not found for compressed asset: {0} {1}", compressedAsset.RelativePath, compressedAsset.Identity); - throw new InvalidOperationException($"Endpoints not found for compressed asset: {compressedAsset.Identity}"); + Log.LogWarning("Endpoints not found for compressed asset: '{0}' '{1}'. Skipping asset", compressedAsset.RelativePath, compressedAsset.Identity); + continue; } if (!endpointsByAsset.TryGetValue(relatedAsset.Identity, out var relatedAssetEndpoints)) { - Log.LogWarning("Endpoints not found for related asset: {0}", relatedAsset.Identity); - throw new InvalidOperationException($"Endpoints not found for related asset: {relatedAsset.Identity}"); + Log.LogWarning("Endpoints not found for related asset: '{0}'. Skipping asset", relatedAsset.Identity); + continue; } Log.LogMessage("Processing compressed asset: {0}", compressedAsset.Identity); diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs index a4a7357fa0c6..e78120ac75c0 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs @@ -29,7 +29,19 @@ public override bool Execute() var candidates = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray(); var assetsToUpdate = new List(); - var candidatesByIdentity = candidates.ToDictionary(asset => asset.Identity, OSPath.PathComparer); + var candidatesByIdentity = new Dictionary(CandidateAssets.Length, OSPath.PathComparer); + for (var i = 0; i < candidates.Length; i++) + { + var candidate = candidates[i]; + if (candidatesByIdentity.ContainsKey(candidate.Identity)) + { + Log.LogMessage(MessageImportance.Low, + "Detected duplicated asset '{0}'. Skipping the asset because it was already processed.", + candidate.Identity); + return false; + } + candidatesByIdentity[candidate.Identity] = candidate; + } foreach (var candidate in candidates) { diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs index 48524f5b6b6a..168a6cd0be20 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs @@ -1128,6 +1128,60 @@ string candidate when candidate.EndsWith(Path.Combine("compressed", "candidate.j ]); } + [Fact] + public void AppliesContentNegotiationRules_Issue59291() + { + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var task = new ApplyCompressionNegotiation + { + BuildEngine = buildEngine.Object, + CandidateAssets = [ + CreateCandidate( + @"D:\work\investigations\aspnet-59291-repro\WebWorkers.Issue4\obj\Release\net9.0\compressed\4t6f7o5fzg-j538s5lvtp.gz", + "SpawnDev.BlazorJS.WebWorkers", + "Package", + "spawndev.blazorjs.webworkers.empty.js.gz", + "All", + "All", + "pjmz1tbrkh", + "5QGGFe52U8tL8jO+4zNzscmjjpZSY1o1BGIJkV/RmI4=", + @"D:\Nuget\spawndev.blazorjs.webworkers\2.5.30\staticwebassets\spawndev.blazorjs.webworkers.empty.js", + "Content-Encoding", + "gzip" + )], + CandidateEndpoints = [.. new StaticWebAssetEndpoint[] + { + new() { + Route = "spawndev.blazorjs.webworkers.empty.js.gz", + AssetFile = @"D:\work\investigations\aspnet-59291-repro\RazorClassLibrary1\obj\Release\net9.0\compressed\4t6f7o5fzg-j538s5lvtp.gz", + ResponseHeaders = [ + new StaticWebAssetEndpointResponseHeader { Name = "Accept-Ranges", Value = "bytes" }, + new StaticWebAssetEndpointResponseHeader { Name = "Cache-Control", Value = "no-cache" }, + new StaticWebAssetEndpointResponseHeader { Name = "Content-Encoding", Value = "gzip" }, + new StaticWebAssetEndpointResponseHeader { Name = "Content-Length", Value = "471" }, + new StaticWebAssetEndpointResponseHeader { Name = "Content-Type", Value = "text/javascript" }, + new StaticWebAssetEndpointResponseHeader { Name = "ETag", Value = "\u00225QGGFe52U8tL8jO\u002B4zNzscmjjpZSY1o1BGIJkV/RmI4=\u0022" }, + new StaticWebAssetEndpointResponseHeader { Name = "Last-Modified", Value = "Fri, 09 Feb 2024 02:37:40 GMT" }, + new StaticWebAssetEndpointResponseHeader { Name = "Vary", Value = "Content-Encoding" } + ], + EndpointProperties = [ + new StaticWebAssetEndpointProperty { Name = "integrity", Value = "sha256-5QGGFe52U8tL8jO\u002B4zNzscmjjpZSY1o1BGIJkV/RmI4=" } + ], + Selectors = [] + } }.Select(e => e.ToTaskItem())], + }; + + // Act + var result = task.Execute(); + + // Assert + result.Should().Be(true); + } + [Fact] public void AppliesContentNegotiationRules_ProcessesNewCompressedFormatsWhenAvailable() {