File: GeneratePackageAssetsManifestFile.cs
Web Access
Project: src\src\sdk\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;

// Generates a JSON manifest file containing both static web asset and endpoint data
// for inclusion in a NuGet package. Replaces the pair of XML .props generators
// (GenerateStaticWebAssetsPropsFile + GenerateStaticWebAssetEndpointsPropsFile) for
// the new .targets-based packaging path (net11.0+).
public class GeneratePackageAssetsManifestFile : Task
{
    [Required]
    public ITaskItem[] StaticWebAssets { get; set; }

    [Required]
    public ITaskItem[] StaticWebAssetEndpoints { get; set; }

    [Required]
    public string TargetManifestPath { get; set; }

    public string PackagePathPrefix { get; set; } = "staticwebassets";

    public string FrameworkPattern { get; set; }

    public override bool Execute()
    {
        if (StaticWebAssets.Length == 0)
        {
            return !Log.HasLoggedErrors;
        }

        var tokenResolver = StaticWebAssetTokenResolver.Instance;

        var frameworkMatcher = CreateFrameworkMatcher();
        var hasFrameworkMatcher = frameworkMatcher != null;
        var matchContext = hasFrameworkMatcher ? StaticWebAssetGlobMatcher.CreateMatchContext() : default;

        // Parse all assets once and pre-compute package-relative paths.
        var parsedAssets = StaticWebAsset.FromTaskItemGroup(StaticWebAssets);
        var (identityToPackagePath, packagePaths) = ComputePackagePaths(parsedAssets, tokenResolver);

        var assets = BuildManifestAssets(parsedAssets, packagePaths, identityToPackagePath, frameworkMatcher, matchContext);
        if (assets == null)
        {
            return false;
        }

        var manifestEndpoints = BuildManifestEndpoints(identityToPackagePath);
        if (manifestEndpoints == null)
        {
            return false;
        }

        var manifest = new StaticWebAssetPackageManifest
        {
            Version = StaticWebAssetPackageManifest.CurrentVersion,
            ManifestType = StaticWebAssetPackageManifest.PackageManifestType,
            Assets = assets,
            Endpoints = manifestEndpoints,
        };

        this.PersistFileIfChanged(manifest, TargetManifestPath,
            StaticWebAssetsJsonSerializerContext.RelaxedEscaping.StaticWebAssetPackageManifest);

        return !Log.HasLoggedErrors;
    }

    private StaticWebAssetGlobMatcher CreateFrameworkMatcher()
    {
        if (string.IsNullOrEmpty(FrameworkPattern))
        {
            return null;
        }

        var patterns = FrameworkPattern
            .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
            .Select(s => s.Trim())
            .ToArray();
        return new StaticWebAssetGlobMatcherBuilder()
            .AddIncludePatterns(patterns)
            .Build();
    }

    private (Dictionary<string, string> IdentityToPackagePath, string[] PackagePaths) ComputePackagePaths(
        StaticWebAsset[] parsedAssets, StaticWebAssetTokenResolver tokenResolver)
    {
        var identityToPackagePath = new Dictionary<string, string>(parsedAssets.Length, Utils.OSPath.PathComparer);
        var packagePaths = new string[parsedAssets.Length];
        for (var i = 0; i < parsedAssets.Length; i++)
        {
            var packagePath = parsedAssets[i].ComputeTargetPath(PackagePathPrefix, '/', tokenResolver, TokenResolveMode.Pack);
            identityToPackagePath[parsedAssets[i].Identity] = packagePath;
            packagePaths[i] = packagePath;
        }
        return (identityToPackagePath, packagePaths);
    }

    private Dictionary<string, StaticWebAsset> BuildManifestAssets(
        StaticWebAsset[] parsedAssets,
        string[] packagePaths,
        Dictionary<string, string> identityToPackagePath,
        StaticWebAssetGlobMatcher frameworkMatcher,
        StaticWebAssetGlobMatcher.MatchContext matchContext)
    {
        var hasFrameworkMatcher = frameworkMatcher != null;
        var assets = new Dictionary<string, StaticWebAsset>(OSPath.PathComparer);

        // Sort indices by BasePath then RelativePath for deterministic output.
        var indices = Enumerable.Range(0, parsedAssets.Length)
            .OrderBy(i => parsedAssets[i].BasePath, StringComparer.OrdinalIgnoreCase)
            .ThenBy(i => parsedAssets[i].RelativePath, StringComparer.OrdinalIgnoreCase);

        foreach (var i in indices)
        {
            var asset = parsedAssets[i];
            var relativePath = asset.RelativePath;
            var packagePath = packagePaths[i];

            var emittedSourceType = StaticWebAsset.SourceTypes.Package;
            if (hasFrameworkMatcher)
            {
                matchContext.SetPathAndReinitialize(relativePath.AsSpan());
                var match = frameworkMatcher.Match(matchContext);
                if (match.IsMatch)
                {
                    emittedSourceType = StaticWebAsset.SourceTypes.Framework;
                }
            }

            // Remap RelatedAsset from build-time absolute path to package-relative path
            var relatedAssetValue = asset.RelatedAsset;
            if (!string.IsNullOrEmpty(relatedAssetValue) &&
                identityToPackagePath.TryGetValue(relatedAssetValue, out var remappedRelatedAsset))
            {
                relatedAssetValue = remappedRelatedAsset;
            }
            else if (!string.IsNullOrEmpty(relatedAssetValue))
            {
                Log.LogError(
                    "Asset '{0}' has RelatedAsset '{1}' which could not be mapped to a package-relative path. " +
                    "This indicates a graph inconsistency — the related asset is not part of the package.",
                    asset.Identity, relatedAssetValue);
                return null;
            }

            var manifestAsset = new StaticWebAsset(asset)
            {
                Identity = packagePath,
                SourceType = emittedSourceType,
                RelatedAsset = relatedAssetValue
            };

            assets[packagePath] = manifestAsset;
        }

        return assets;
    }

    private StaticWebAssetEndpoint[] BuildManifestEndpoints(Dictionary<string, string> identityToPackagePath)
    {
        var endpoints = StaticWebAssetEndpoint.FromItemGroup(StaticWebAssetEndpoints);
        var manifestEndpoints = new List<StaticWebAssetEndpoint>();
        foreach (var endpoint in endpoints.OrderBy(e => e.Route, StringComparer.OrdinalIgnoreCase)
            .ThenBy(e => e.AssetFile, StringComparer.OrdinalIgnoreCase))
        {
            // Remap AssetFile to package-relative path
            if (identityToPackagePath.TryGetValue(endpoint.AssetFile, out var packageRelativePath))
            {
                endpoint.AssetFile = packageRelativePath;
            }
            else
            {
                Log.LogError(
                    "Endpoint '{0}' references AssetFile '{1}' which could not be mapped to a package-relative path. " +
                    "This indicates a graph inconsistency — the referenced asset is not part of the package.",
                    endpoint.Route, endpoint.AssetFile);
                return null;
            }
            manifestEndpoints.Add(endpoint);
        }
        return manifestEndpoints.ToArray();
    }
}