File: ReadPackageAssetsManifest.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 System.Text.Json;
using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
using Microsoft.Build.Framework;

namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;

// Reads StaticWebAssetPackageManifest items, deserializes the JSON manifests,
// applies group filtering, and emits matching assets and endpoints as MSBuild items.
// This replaces the eager import of XML .props files with a task-based read-and-filter approach.
public class ReadPackageAssetsManifest : Task
{
    [Required]
    public ITaskItem[] PackageManifests { get; set; }

    public ITaskItem[] StaticWebAssetGroups { get; set; }

    public string IntermediateOutputPath { get; set; }

    public string ProjectPackageId { get; set; }

    public string ProjectBasePath { get; set; }

    [Output]
    public ITaskItem[] Assets { get; set; }

    [Output]
    public ITaskItem[] Endpoints { get; set; }

    public override bool Execute()
    {
        if (string.IsNullOrEmpty(IntermediateOutputPath))
        {
            Log.LogError("IntermediateOutputPath is required.");
            return false;
        }

        var groupLookup = StaticWebAssetGroup.FromItemGroup(StaticWebAssetGroups);
        var allAssets = new List<StaticWebAsset>();
        var allEndpoints = new List<StaticWebAssetEndpoint>();

        foreach (var manifestItem in PackageManifests)
        {
            var manifestPath = manifestItem.ItemSpec;
            var sourceId = manifestItem.GetMetadata("SourceId");
            var packageRoot = manifestItem.GetMetadata("PackageRoot");
            var contentRoot = manifestItem.GetMetadata("ContentRoot");

            if (!File.Exists(manifestPath))
            {
                Log.LogError("Package manifest file '{0}' not found.", manifestPath);
                return false;
            }

            var manifest = ReadManifest(manifestPath);
            if (manifest == null)
            {
                return false;
            }

            if (manifest.Assets == null || manifest.Assets.Count == 0)
            {
                continue;
            }

            // Copy manifest assets — Identity and RelatedAsset are already
            // package-relative paths as written by GeneratePackageAssetsManifestFile.
            var assets = new StaticWebAsset[manifest.Assets.Count];
            var index = 0;
            foreach (var entry in manifest.Assets)
            {
                var asset = new StaticWebAsset(entry.Value);
                assets[index++] = asset;
            }

            var (includedAssets, excludedPaths) = StaticWebAsset.FilterByGroup(assets, groupLookup, skipDeferred: true);

            // Filter endpoints on raw package-relative paths before resolving anything.
            var endpointGroups = StaticWebAssetEndpointGroup.CreateEndpointGroups(manifest.Endpoints ?? []);
            var (_, includedEndpoints) = StaticWebAssetEndpointGroup.ComputeFilteredEndpoints(endpointGroups, excludedPaths);

            if (!ResolveAssetsAndEndpoints(includedAssets, includedEndpoints, sourceId, packageRoot, contentRoot))
            {
                return false;
            }

            allAssets.AddRange(includedAssets);
            allEndpoints.AddRange(includedEndpoints);
        }

        Assets = allAssets.Select(asset => asset.ToTaskItem()).ToArray();
        Endpoints = StaticWebAssetEndpoint.ToTaskItems(allEndpoints);

        return !Log.HasLoggedErrors;
    }

    // Resolve paths — framework assets materialize into the fx intermediate
    // folder; everything else resolves against the package root.
    // Build a lookup for framework asset identities so RelatedAsset and
    // endpoint.AssetFile references can resolve to the materialized path.
    private bool ResolveAssetsAndEndpoints(
        List<StaticWebAsset> assets,
        List<StaticWebAssetEndpoint> endpoints,
        string sourceId,
        string packageRoot,
        string contentRoot)
    {
        var fxDir = Path.Combine(IntermediateOutputPath, "fx", sourceId);
        var frameworkPaths = new Dictionary<string, string>(OSPath.PathComparer);
        var normalizedContentRoot = StaticWebAsset.NormalizeContentRootPath(contentRoot);

        foreach (var asset in assets)
        {
            if (StaticWebAsset.SourceTypes.IsFramework(asset.SourceType))
            {
                var resolvedRelativePath = StaticWebAssetPathPattern.PathWithoutTokens(asset.RelativePath);
                var destPath = Path.GetFullPath(Path.Combine(fxDir, resolvedRelativePath));
                frameworkPaths[asset.Identity] = destPath;
                MaterializeFrameworkAsset(asset, packageRoot, fxDir, destPath);
                if (Log.HasLoggedErrors)
                {
                    return false;
                }
            }
            else
            {
                asset.Identity = ResolvePath(packageRoot, asset.Identity);
                asset.OriginalItemSpec = asset.Identity;
                asset.ContentRoot = normalizedContentRoot;
            }

            if (!string.IsNullOrEmpty(asset.RelatedAsset))
            {
                asset.RelatedAsset = frameworkPaths.TryGetValue(asset.RelatedAsset, out var fxRelated)
                    ? fxRelated
                    : ResolvePath(packageRoot, asset.RelatedAsset);
            }
        }

        foreach (var endpoint in endpoints)
        {
            endpoint.AssetFile = frameworkPaths.TryGetValue(endpoint.AssetFile, out var fxEp)
                ? fxEp
                : ResolvePath(packageRoot, endpoint.AssetFile);
        }

        return true;
    }

    private StaticWebAssetPackageManifest ReadManifest(string manifestPath)
    {
        StaticWebAssetPackageManifest manifest;
        try
        {
            var json = File.ReadAllBytes(manifestPath);
            manifest = JsonSerializer.Deserialize(json,
                StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest);
        }
        catch (Exception ex)
        {
            Log.LogError("Failed to read package manifest '{0}': {1}", manifestPath, ex.Message);
            return null;
        }

        if (manifest is null)
        {
            Log.LogError("Package manifest '{0}' deserialized to null.", manifestPath);
            return null;
        }

        if (manifest.Version != StaticWebAssetPackageManifest.CurrentVersion)
        {
            Log.LogError("Unsupported package manifest version {0} in '{1}'. Expected version {2}.", manifest.Version, manifestPath, StaticWebAssetPackageManifest.CurrentVersion);
            return null;
        }

        if (!string.Equals(manifest.ManifestType, StaticWebAssetPackageManifest.PackageManifestType, StringComparison.Ordinal))
        {
            Log.LogError("Unexpected manifest type '{0}' in '{1}'. Expected '{2}'.", manifest.ManifestType, manifestPath, StaticWebAssetPackageManifest.PackageManifestType);
            return null;
        }

        return manifest;
    }

    private void MaterializeFrameworkAsset(
        StaticWebAsset asset,
        string packageRoot,
        string fxDir,
        string destPath)
    {
        var sourcePath = ResolvePath(packageRoot, asset.Identity);

        if (!File.Exists(sourcePath))
        {
            Log.LogError("Source file '{0}' does not exist for framework asset materialization.", sourcePath);
            return;
        }

        Directory.CreateDirectory(Path.GetDirectoryName(destPath));

        if (!File.Exists(destPath) || File.GetLastWriteTimeUtc(sourcePath) > File.GetLastWriteTimeUtc(destPath))
        {
            File.Copy(sourcePath, destPath, overwrite: true);
        }

        asset.Identity = destPath;
        asset.OriginalItemSpec = destPath;
        asset.SourceType = StaticWebAsset.SourceTypes.Discovered;
        asset.SourceId = ProjectPackageId;
        asset.ContentRoot = StaticWebAsset.NormalizeContentRootPath(fxDir);
        asset.BasePath = ProjectBasePath;
        asset.AssetMode = StaticWebAsset.AssetModes.CurrentProject;
    }

    private static string ResolvePath(string packageRoot, string relativePath)
    {
        return Path.GetFullPath(Path.Combine(packageRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
    }
}