Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Copyright (c) .NET Foundation. All rights reserved.

<UsingTask TaskName="Microsoft.AspNetCore.StaticWebAssets.Tasks.BrotliCompress" AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.StaticWebAssets.Tasks.GZipCompress" AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.StaticWebAssets.Tasks.DiscoverPrecompressedAssets" AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.StaticWebAssets.Tasks.ResolveCompressedAssets" AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.StaticWebAssets.Tasks.ApplyCompressionNegotiation" AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" />

Expand Down Expand Up @@ -225,7 +226,7 @@ Copyright (c) .NET Foundation. All rights reserved.
</DefineStaticWebAssets>

<DefineStaticWebAssetEndpoints
CandidateAssets="@(_CompressionBuildStaticWebAsset)"
CandidateAssets="@(_CompressionBuildStaticWebAsset);@(_PrecompressedStaticWebAssets)"
ExistingEndpoints="@(StaticWebAssetEndpoint)"
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
>
Expand Down Expand Up @@ -276,6 +277,27 @@ Copyright (c) .NET Foundation. All rights reserved.
</Target>

<Target Name="ResolveBuildCompressedStaticWebAssetsConfiguration" DependsOnTargets="ResolveStaticWebAssetsInputs;$(ResolveCompressedFilesDependsOn)">
<!-- There might be assets that are precompressed on packages or that are precompressed by other tools.
In this case, we need to detect those assets, remove them and their endpoints, adjust the asset definition
and recreate the endpoints for those assets as the original ones will not be correct.
-->
<DiscoverPrecompressedAssets CandidateAssets="@(StaticWebAsset)">
<Output TaskParameter="DiscoveredCompressedAssets" ItemName="_PrecompressedStaticWebAssets" />
</DiscoverPrecompressedAssets>

<FilterStaticWebAssetEndpoints Condition="'@(_PrecompressedStaticWebAssets)' != ''"
Endpoints="@(StaticWebAssetEndpoint)"
Assets="@(_PrecompressedStaticWebAssets)"
Filters=""
>
<Output TaskParameter="FilteredEndpoints" ItemName="_PrecompressedEndpointsToRemove" />
</FilterStaticWebAssetEndpoints>

<ItemGroup Condition="'@(_PrecompressedStaticWebAssets)' != ''">
<StaticWebAssetEndpoint Remove="@(_PrecompressedEndpointsToRemove)" />
<StaticWebAsset Remove="@(_PrecompressedStaticWebAssets)" />
<StaticWebAsset Include="@(_PrecompressedStaticWebAssets)" />
</ItemGroup>
Comment on lines +280 to +300
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Discover existing pre-compressed assets.
  • Remove their original definitions.
  • Provide an updated definition.
  • Remove the associated endpoint definitions and re-generate new endpoints.


<ResolveCompressedAssets
CandidateAssets="@(StaticWebAsset)"
Expand Down
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;
}
}
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}"));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.StaticWebAssets.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Moq;
using NuGet.ContentModel;
using NuGet.Packaging.Core;

namespace Microsoft.NET.Sdk.Razor.Tests;

Expand Down Expand Up @@ -70,6 +72,74 @@ public void ResolvesExplicitlyProvidedAssets()
task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br");
}

[Fact]
public void InfersPreCompressedAssetsCorrectly()
{
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 = "xtxxf3hu2r",
RelatedAsset = string.Empty,
ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"),
SourceType = StaticWebAsset.SourceTypes.Discovered,
Integrity = "hRQyftXiu1lLX2P9Ly9xa4gHJgLeR1uGN5qegUobtGo=",
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 = "es13vhk42b",
RelatedAsset = string.Empty,
ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"),
SourceType = StaticWebAsset.SourceTypes.Discovered,
Integrity = "zs5Fd3XI6+g9f4N1SFLVdgghuiqdvq+nETAjTbvVxx4=",
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 ResolveCompressedAssets
{
OutputPath = OutputBasePath,
CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()],
Formats = "gzip",
BuildEngine = buildEngine.Object
};

var result = task.Execute();

result.Should().BeTrue();
task.AssetsToCompress.Should().HaveCount(0);
}

[Fact]
public void ResolvesAssetsMatchingIncludePattern()
{
Expand Down
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"
]
Loading