| File: GenerateStaticWebAssetsManifest.cs | Web Access |
| Project: 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; [MSBuildMultiThreadableTask] public class GenerateStaticWebAssetsManifest : Task, IMultiThreadableTask { /// <inheritdoc/> public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; [Required] public string Source { get; set; } [Required] public string BasePath { get; set; } [Required] public string Mode { get; set; } [Required] public string ManifestType { get; set; } [Required] public ITaskItem[] ReferencedProjectsConfigurations { get; set; } [Required] public ITaskItem[] DiscoveryPatterns { get; set; } [Required] public ITaskItem[] Assets { get; set; } [Required] public ITaskItem[] Endpoints { get; set; } [Required] public string ManifestPath { get; set; } public string ManifestCacheFilePath { get; set; } public override bool Execute() { try { var assets = StaticWebAsset.FromTaskItemGroup(Assets, TaskEnvironment, validate: true); Array.Sort(assets, (l, r) => string.CompareOrdinal(l.Identity, r.Identity)); var endpoints = FilterPublishEndpointsIfNeeded(assets); Array.Sort(endpoints, (l, r) => string.CompareOrdinal(l.Route, r.Route) switch { 0 => string.CompareOrdinal(l.AssetFile, r.AssetFile), int result => result, }); Log.LogMessage(MessageImportance.Low, "Generating manifest for '{0}' assets and '{1}' endpoints", assets.Length, endpoints.Length); var assetsByTargetPath = GroupAssetsByTargetPath(assets); var groupSet = new HashSet<string>(StringComparer.Ordinal); foreach (var group in assetsByTargetPath) { if (!StaticWebAsset.ValidateAssetGroup(group.Key, group.Value, out var reason, groupSet)) { Log.LogError(reason); return false; } } var discoveryPatterns = DiscoveryPatterns .OrderBy(a => a.ItemSpec) .Select(StaticWebAssetsDiscoveryPattern.FromTaskItem) .ToArray(); var referencedProjectsConfiguration = ReferencedProjectsConfigurations.OrderBy(a => a.ItemSpec) .Select(StaticWebAssetsManifest.ReferencedProjectConfiguration.FromTaskItem) .ToArray(); PersistManifest( StaticWebAssetsManifest.Create( Source, BasePath, Mode, ManifestType, referencedProjectsConfiguration, discoveryPatterns, [.. assets], endpoints)); } catch (Exception ex) { Log.LogErrorFromException(ex, showStackTrace: true, showDetail: true, file: null); } return !Log.HasLoggedErrors; } private StaticWebAssetEndpoint[] FilterPublishEndpointsIfNeeded(StaticWebAsset[] assets) { // Only include endpoints for assets that are going to be available in production. We do the filtering // inside the manifest because its cumbersome to do it in MSBuild directly. if (StaticWebAssetsManifest.ManifestTypes.IsPublish(ManifestType)) { var assetsByIdentity = assets.ToDictionary(a => a.Identity, a => a, OSPath.PathComparer); var filteredEndpoints = new List<StaticWebAssetEndpoint>(); foreach (var endpoint in Endpoints.Select(StaticWebAssetEndpoint.FromTaskItem)) { if (assetsByIdentity.ContainsKey(endpoint.AssetFile)) { filteredEndpoints.Add(endpoint); Log.LogMessage(MessageImportance.Low, $"Accepted endpoint: Route='{endpoint.Route}', AssetFile='{endpoint.AssetFile}'"); } else { Log.LogMessage(MessageImportance.Low, $"Filtered out endpoint: Endpoint='{endpoint.Route}' AssetFile='{endpoint.AssetFile}'"); } } return [.. filteredEndpoints]; } return StaticWebAssetEndpoint.FromItemGroup(Endpoints); } private void PersistManifest(StaticWebAssetsManifest manifest) { // Absolutize once; preserve original paths for log messages. string absolutizedManifestPath = !string.IsNullOrWhiteSpace(ManifestPath) ? TaskEnvironment.GetAbsolutePath(ManifestPath) : ManifestPath; bool isManifestCacheFileConfigured = !string.IsNullOrWhiteSpace(ManifestCacheFilePath); string absolutizedManifestCacheFilePath = isManifestCacheFileConfigured ? TaskEnvironment.GetAbsolutePath(ManifestCacheFilePath) : ManifestCacheFilePath; var cacheFileExists = isManifestCacheFileConfigured && File.Exists(absolutizedManifestCacheFilePath); var manifestFileExists = File.Exists(absolutizedManifestPath); var existingManifestHash = cacheFileExists ? File.ReadAllText(absolutizedManifestCacheFilePath) : manifestFileExists ? StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(absolutizedManifestPath)).Hash : ""; if (!manifestFileExists || !string.Equals(manifest.Hash, existingManifestHash, StringComparison.Ordinal)) { var data = JsonSerializer.SerializeToUtf8Bytes(manifest, StaticWebAssetsJsonSerializerContext.RelaxedEscaping.StaticWebAssetsManifest); if (!manifestFileExists) { Log.LogMessage(MessageImportance.Low, $"Creating manifest because manifest file '{ManifestPath}' does not exist."); } else { Log.LogMessage(MessageImportance.Low, $"Updating manifest because manifest version '{manifest.Hash}' is different from existing manifest hash '{existingManifestHash}'."); } File.WriteAllBytes(absolutizedManifestPath, data); if (isManifestCacheFileConfigured) { File.WriteAllText(absolutizedManifestCacheFilePath, manifest.Hash); } } else { Log.LogMessage(MessageImportance.Low, $"Skipping manifest updated because manifest version '{manifest.Hash}' has not changed."); } } private static Dictionary<string, (StaticWebAsset First, StaticWebAsset Second, List<StaticWebAsset> Other)> GroupAssetsByTargetPath(StaticWebAsset[] assets) { var result = new Dictionary<string, (StaticWebAsset First, StaticWebAsset Second, List<StaticWebAsset> Other)>(StringComparer.OrdinalIgnoreCase); foreach (var asset in assets) { var targetPath = asset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); if (result.TryGetValue(targetPath, out var existing)) { if (existing.Second == null) { // We have first but not second result[targetPath] = (existing.First, asset, null); } else { // We already have first and second, add to rest existing.Other ??= []; existing.Other.Add(asset); } } else { // First asset with this path result.Add(targetPath, (asset, null, null)); } } return result; } }