-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[StaticWebAssets] Detects pre-compressed assets #44976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mkArtakMSFT
merged 11 commits into
release/9.0.1xx
from
javiercn/discover-precompressed-assets
Nov 20, 2024
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
ebf020f
Detect pre-compressed assets
javiercn 1f2461a
E2E test
javiercn 286be41
Make sure endpoints are re-generated
javiercn 3767335
E2E test
javiercn cc4f402
Clean baselines
javiercn 78331a9
Improved E2E test
javiercn 179c57d
Add publish test and improve the assertions
javiercn 1b958c8
Fix test
javiercn 638635a
Switch to use a Dictionary
javiercn ff4758b
Cleanup
javiercn b7d2416
Remove unnecessary comment
javiercn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Microsoft.Build.Framework; | ||
|
|
||
| namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; | ||
|
|
||
| public class DiscoverPrecompressedAssets : Task | ||
| { | ||
| private const string GzipAssetTraitValue = "gzip"; | ||
| private const string BrotliAssetTraitValue = "br"; | ||
|
|
||
| public ITaskItem[] CandidateAssets { get; set; } | ||
|
|
||
| [Output] | ||
| public ITaskItem[] DiscoveredCompressedAssets { get; set; } | ||
|
|
||
| public override bool Execute() | ||
| { | ||
| if (CandidateAssets is null) | ||
| { | ||
| Log.LogMessage( | ||
| MessageImportance.Low, | ||
| "Skipping task '{0}' because no candidate assets for compression were specified.", | ||
| nameof(ResolveCompressedAssets)); | ||
| return true; | ||
| } | ||
|
|
||
| var candidates = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray(); | ||
| var assetsToUpdate = new List<ITaskItem>(); | ||
|
|
||
| var candidatesByIdentity = candidates.ToDictionary(asset => asset.Identity, OSPath.PathComparer); | ||
|
|
||
| foreach (var candidate in candidates) | ||
| { | ||
| if (HasCompressionExtension(candidate.RelativePath) && | ||
| // We only care about assets that are not already considered compressed | ||
| !IsCompressedAsset(candidate) && | ||
| // The candidate doesn't already have a related asset | ||
| string.IsNullOrEmpty(candidate.RelatedAsset)) | ||
| { | ||
| Log.LogMessage( | ||
| MessageImportance.Low, | ||
| "The asset '{0}' was detected as compressed but it didn't specify a related asset.", | ||
| candidate.Identity); | ||
| var relatedAsset = FindRelatedAsset(candidate, candidatesByIdentity); | ||
| if (relatedAsset is null) | ||
| { | ||
| Log.LogMessage( | ||
| MessageImportance.Low, | ||
| "The asset '{0}' was detected as compressed but the related asset with relative path '{1}' was not found.", | ||
| candidate.Identity, | ||
| Path.GetFileNameWithoutExtension(candidate.RelativePath)); | ||
| continue; | ||
| } | ||
|
|
||
| Log.LogMessage( | ||
| "The asset '{0}' was detected as compressed and the related asset '{1}' was found.", | ||
| candidate.Identity, | ||
| relatedAsset.Identity); | ||
| UpdateCompressedAsset(candidate, relatedAsset); | ||
| assetsToUpdate.Add(candidate.ToTaskItem()); | ||
| } | ||
| } | ||
|
|
||
| DiscoveredCompressedAssets = [.. assetsToUpdate]; | ||
|
|
||
| return !Log.HasLoggedErrors; | ||
| } | ||
|
|
||
| private StaticWebAsset FindRelatedAsset(StaticWebAsset candidate, IDictionary<string, StaticWebAsset> candidates) | ||
| { | ||
| // The only pattern that we support is a related asset that lives in the same directory, with the same name, | ||
| // but without the compression extension. In any other case we are not going to consider the assets related | ||
| // and an error will occur. | ||
| var identityWithoutExtension = candidate.Identity.Substring(0, candidate.Identity.Length - 3); // We take advantage we know the extension is .br or .gz. | ||
| return candidates.TryGetValue(identityWithoutExtension, out var relatedAsset) ? relatedAsset : null; | ||
| } | ||
|
|
||
| private bool HasCompressionExtension(string relativePath) | ||
| { | ||
| return relativePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) || | ||
| relativePath.EndsWith(".br", StringComparison.OrdinalIgnoreCase); | ||
| } | ||
|
|
||
| private static bool IsCompressedAsset(StaticWebAsset asset) | ||
| => string.Equals("Content-Encoding", asset.AssetTraitName, StringComparison.Ordinal); | ||
|
|
||
| private void UpdateCompressedAsset(StaticWebAsset asset, StaticWebAsset relatedAsset) | ||
| { | ||
| string fileExtension; | ||
| string assetTraitValue; | ||
|
|
||
| if (!asset.RelativePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| fileExtension = ".br"; | ||
| assetTraitValue = BrotliAssetTraitValue; | ||
| } | ||
| else | ||
| { | ||
| fileExtension = ".gz"; | ||
| assetTraitValue = GzipAssetTraitValue; | ||
| } | ||
|
|
||
| var originalItemSpec = asset.OriginalItemSpec; | ||
| var relativePath = relatedAsset.EmbedTokens(relatedAsset.RelativePath); | ||
|
|
||
| asset.RelativePath = $"{relativePath}{fileExtension}"; | ||
| asset.OriginalItemSpec = relatedAsset.Identity; | ||
| asset.RelatedAsset = relatedAsset.Identity; | ||
| asset.AssetRole = "Alternative"; | ||
| asset.AssetTraitName = "Content-Encoding"; | ||
| asset.AssetTraitValue = assetTraitValue; | ||
| } | ||
| } |
109 changes: 109 additions & 0 deletions
109
test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Microsoft.AspNetCore.StaticWebAssets.Tasks; | ||
| using Microsoft.Build.Framework; | ||
| using Moq; | ||
|
|
||
| namespace Microsoft.NET.Sdk.Razor.Tests; | ||
|
|
||
| public class DiscoverPrecompressedAssetsTest | ||
| { | ||
| public string ItemSpec { get; } | ||
|
|
||
| public string OriginalItemSpec { get; } | ||
|
|
||
| public string OutputBasePath { get; } | ||
|
|
||
| public DiscoverPrecompressedAssetsTest() | ||
| { | ||
| OutputBasePath = Path.Combine(TestContext.Current.TestExecutionDirectory, nameof(ResolveCompressedAssetsTest)); | ||
| ItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); | ||
| OriginalItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void DiscoversPrecompressedAssetsCorrectly() | ||
| { | ||
| var errorMessages = new List<string>(); | ||
| var buildEngine = new Mock<IBuildEngine>(); | ||
| buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) | ||
| .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message)); | ||
|
|
||
| var uncompressedCandidate = new StaticWebAsset | ||
| { | ||
| Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), | ||
| RelativePath = "js/site#[.{fingerprint}]?.js", | ||
| BasePath = "_content/Test", | ||
| AssetMode = StaticWebAsset.AssetModes.All, | ||
| AssetKind = StaticWebAsset.AssetKinds.All, | ||
| AssetMergeSource = string.Empty, | ||
| SourceId = "Test", | ||
| CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, | ||
| Fingerprint = "uncompressed", | ||
| RelatedAsset = string.Empty, | ||
| ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), | ||
| SourceType = StaticWebAsset.SourceTypes.Discovered, | ||
| Integrity = "uncompressed-integrity", | ||
| AssetRole = StaticWebAsset.AssetRoles.Primary, | ||
| AssetMergeBehavior = string.Empty, | ||
| AssetTraitValue = string.Empty, | ||
| AssetTraitName = string.Empty, | ||
| OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), | ||
| CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest | ||
| }; | ||
|
|
||
| var compressedCandidate = new StaticWebAsset | ||
| { | ||
| Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), | ||
| RelativePath = "js/site.js#[.{fingerprint}]?.gz", | ||
| BasePath = "_content/Test", | ||
| AssetMode = StaticWebAsset.AssetModes.All, | ||
| AssetKind = StaticWebAsset.AssetKinds.All, | ||
| AssetMergeSource = string.Empty, | ||
| SourceId = "Test", | ||
| CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, | ||
| Fingerprint = "compressed", | ||
| RelatedAsset = string.Empty, | ||
| ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), | ||
| SourceType = StaticWebAsset.SourceTypes.Discovered, | ||
| Integrity = "compressed-integrity", | ||
| AssetRole = StaticWebAsset.AssetRoles.Primary, | ||
| AssetMergeBehavior = string.Empty, | ||
| AssetTraitValue = string.Empty, | ||
| AssetTraitName = string.Empty, | ||
| OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), | ||
| CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest | ||
| }; | ||
|
|
||
| var task = new DiscoverPrecompressedAssets | ||
| { | ||
| CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], | ||
| BuildEngine = buildEngine.Object | ||
| }; | ||
|
|
||
| var result = task.Execute(); | ||
|
|
||
| result.Should().BeTrue(); | ||
| task.DiscoveredCompressedAssets.Should().ContainSingle(); | ||
| var asset = task.DiscoveredCompressedAssets[0]; | ||
| asset.ItemSpec.Should().Be(compressedCandidate.Identity); | ||
| asset.GetMetadata("RelatedAsset").Should().Be(uncompressedCandidate.Identity); | ||
| asset.GetMetadata("OriginalItemSpec").Should().Be(uncompressedCandidate.Identity); | ||
| asset.GetMetadata("RelativePath").Should().Be("js/site#[.{fingerprint=uncompressed}]?.js.gz"); | ||
| asset.GetMetadata("AssetRole").Should().Be("Alternative"); | ||
| asset.GetMetadata("AssetTraitName").Should().Be("Content-Encoding"); | ||
| asset.GetMetadata("AssetTraitValue").Should().Be("gzip"); | ||
| asset.GetMetadata("Fingerprint").Should().Be("compressed"); | ||
| asset.GetMetadata("Integrity").Should().Be("compressed-integrity"); | ||
| asset.GetMetadata("CopyToPublishDirectory").Should().Be("PreserveNewest"); | ||
| asset.GetMetadata("CopyToOutputDirectory").Should().Be("Never"); | ||
| asset.GetMetadata("AssetMergeSource").Should().Be(string.Empty); | ||
| asset.GetMetadata("AssetMergeBehavior").Should().Be(string.Empty); | ||
| asset.GetMetadata("AssetKind").Should().Be("All"); | ||
| asset.GetMetadata("AssetMode").Should().Be("All"); | ||
| asset.GetMetadata("SourceId").Should().Be("Test"); | ||
| asset.GetMetadata("SourceType").Should().Be("Discovered"); | ||
| asset.GetMetadata("ContentRoot").Should().Be(Path.Combine(Environment.CurrentDirectory, $"wwwroot{Path.DirectorySeparatorChar}")); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
...k.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.files.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [ | ||
| "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", | ||
| "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css" | ||
| ] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.