File: MergeStaticWebAssets.cs
Web Access
Project: ..\..\..\src\StaticWebAssetsSdk\Tasks\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
using Microsoft.Build.Framework;
 
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
public class MergeStaticWebAssets : Task
{
    [Required]
    public ITaskItem[] CandidateAssets { get; set; }
 
    [Required]
    public ITaskItem[] CandidateDiscoveryPatterns { get; set; }
 
    public string MergeTarget { get; set; } = "";
 
    [Output]
    public ITaskItem[] MergedAssets { get; set; }
 
    [Output]
    public ITaskItem[] MergedDiscoveryPatterns { get; set; }
 
    public override bool Execute()
    {
 
        var assets = StaticWebAsset.FromTaskItemGroup(CandidateAssets);
        Array.Sort(assets, (a, b) => string.CompareOrdinal(a.Identity, b.Identity));
 
        var assetsByTargetPath = assets
            .GroupBy(a => a.ComputeTargetPath("", '/'), StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.ToList());
 
        foreach (var kvp in assetsByTargetPath)
        {
            var group = kvp.Value;
            if (group.Count > 1)
            {
                Log.LogMessage(MessageImportance.Normal, $"Merging '{group.Count}' assets for {kvp.Key}.");
                ApplyMergeRules(group, MergeTarget);
            }
        }
 
        MergedAssets = assetsByTargetPath.Values.SelectMany(g => g).Select(a => a.ToTaskItem()).ToArray();
 
        // We always want to merge the discovery patterns, we just need to remove duplicates if any.           
        var candidates = CandidateDiscoveryPatterns.Select(StaticWebAssetsDiscoveryPattern.FromTaskItem).ToList();
        for (var i = candidates.Count - 1; i > 0; i--)
        {
            var candidate = candidates[i];
            for (var j = i - 1; j >= 0; j--)
            {
                var other = candidates[j];
                if (candidate.Equals(other))
                {
                    Log.LogMessage(MessageImportance.Normal, $"Removing '{candidate.ContentRoot}' because it is a duplicate of '{other.ContentRoot}'.");
                    candidates.RemoveAt(i);
                    break;
                }
            }
        }
 
        MergedDiscoveryPatterns = candidates.Select(a => a.ToTaskItem()).ToArray();
 
        return !Log.HasLoggedErrors;
    }
 
    internal void ApplyMergeRules(List<StaticWebAsset> group, string source)
    {
        // All the assets in the group target the same relative path. The normal outcome for that is a conflict,
        // except when we are merging assets across targets, in which case there are rules to determine what happens.
        StaticWebAsset prototypeItem = null;
        StaticWebAsset build = null;
        StaticWebAsset publish = null;
        StaticWebAsset all = null;
 
        var assetsToRemove = new List<StaticWebAsset>();
        foreach (var item in group)
        {
            prototypeItem ??= item;
            if (!ReferenceEquals(prototypeItem, item) && string.Equals(prototypeItem.Identity, item.Identity, OSPath.PathComparison))
            {
                var assetToRemove = SelectAssetToRemove(prototypeItem, item, source);
                if (assetToRemove != null)
                {
                    Log.LogMessage(MessageImportance.Normal, $"Removing '{assetToRemove.Identity}' because merge behavior is {assetToRemove.AssetMergeBehavior}.");
                    assetsToRemove.Add(assetToRemove);
                    continue;
                }
            }
 
            if (!prototypeItem.HasSourceId(item.SourceId))
            {
                var assetToRemove = SelectAssetToRemove(prototypeItem, item, source);
                if (assetToRemove != null)
                {
                    Log.LogMessage(MessageImportance.Normal, $"Removing '{assetToRemove.Identity}' because merge behavior is {assetToRemove.AssetMergeBehavior}.");
                    assetsToRemove.Add(assetToRemove);
                    continue;
                }
            }
 
            build ??= item.IsBuildOnly() ? item : build;
            if (build != null && item.IsBuildOnly() && !ReferenceEquals(build, item))
            {
                var assetToRemove = SelectAssetToRemove(prototypeItem, item, source);
                if (assetToRemove != null)
                {
                    Log.LogMessage(MessageImportance.Normal, $"Removing '{assetToRemove.Identity}' because merge behavior is {assetToRemove.AssetMergeBehavior}.");
                    assetsToRemove.Add(assetToRemove);
                    continue;
                }
            }
 
            publish ??= item.IsPublishOnly() ? item : publish;
            if (publish != null && item.IsPublishOnly() && !ReferenceEquals(publish, item))
            {
                var assetToRemove = SelectAssetToRemove(prototypeItem, item, source);
                if (assetToRemove != null)
                {
                    Log.LogMessage(MessageImportance.Normal, $"Removing '{assetToRemove.Identity}' because merge behavior is {assetToRemove.AssetMergeBehavior}.");
                    assetsToRemove.Add(assetToRemove);
                    continue;
                }
            }
 
            all ??= item.IsBuildAndPublish() ? item : all;
            if (all != null && item.IsBuildAndPublish() && !ReferenceEquals(all, item))
            {
                var assetToRemove = SelectAssetToRemove(prototypeItem, item, source);
                if (assetToRemove != null)
                {
                    Log.LogMessage(MessageImportance.Normal, $"Removing '{assetToRemove.Identity}' because merge behavior is {assetToRemove.AssetMergeBehavior}.");
                    assetsToRemove.Add(assetToRemove);
                    continue;
                }
            }
        }
 
        foreach (var asset in assetsToRemove)
        {
            group.Remove(asset);
        }
 
        StaticWebAsset SelectAssetToRemove(StaticWebAsset left, StaticWebAsset right, string mergeTarget)
        {
            var leftMergeSource = left.AssetMergeSource;
            var rightMergeSource = right.AssetMergeSource;
            if (string.Equals(leftMergeSource, rightMergeSource, StringComparison.Ordinal))
            {
                Log.LogMessage(MessageImportance.Normal, $"Skipping '{right.Identity}' because it is a duplicate of '{left.Identity}'.");
                return null;
            }
 
            var (targetAsset, sourceAsset) = string.Equals(leftMergeSource, mergeTarget) ? (left, right) : (right, left);
            if (!string.Equals(targetAsset.AssetMergeBehavior, sourceAsset.AssetMergeBehavior, StringComparison.Ordinal))
            {
                Log.LogMessage(MessageImportance.Normal, $"Skipping '{sourceAsset.Identity}' because merge behavior '{sourceAsset.AssetMergeBehavior}' is different from '{targetAsset.AssetMergeBehavior}'.");
                return null;
            }
 
            var behavior = targetAsset.AssetMergeBehavior;
            // PreferTarget: The target asset wins.
            // PreferSource: The source asset wins.
            // Exclude: The assets are not merged, and a failure happens later on during validation.
            return string.Equals(behavior, "PreferTarget", StringComparison.Ordinal) ? targetAsset :
                (string.Equals(behavior, "PreferSource", StringComparison.Ordinal) ? sourceAsset : null);
        }
    }
}