| File: StaticWebAssetsGeneratePackManifest.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.Security.Cryptography; using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.Build.Framework; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; [MSBuildMultiThreadableTask] public class StaticWebAssetsGeneratePackManifest : Task, IMultiThreadableTask { // Since the manifest is only used at build time, it's ok for it to use the relaxed // json escaping (which is also what MVC uses by default) and to produce indented output // since that makes it easier to inspect the manifest when necessary. private static readonly JsonSerializerOptions ManifestSerializationOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = true }; [Required] public ITaskItem[] Assets { get; set; } [Required] public ITaskItem[] AdditionalPackageFiles { get; set; } public ITaskItem[] AdditionalElementsToRemoveFromPacking { get; set; } = []; [Required] public string ManifestPath { get; set; } public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; public override bool Execute() { if (Assets.Length == 0) { // Do nothing if there are no assets to pack. Log.LogMessage(MessageImportance.Low, "Skipping manifest creation because there are no static web assets to pack."); return true; } var packageFiles = new List<StaticWebAssetPackageFile>(); foreach (var file in AdditionalPackageFiles) { packageFiles.Add(new StaticWebAssetPackageFile { Id = file.ItemSpec, PackagePath = file.GetMetadata("PackagePath") }); } foreach (var asset in Assets) { packageFiles.Add(new StaticWebAssetPackageFile { Id = asset.ItemSpec, PackagePath = asset.GetMetadata("TargetPath") }); } packageFiles.Sort((x, y) => string.Compare(x.Id, y.Id, StringComparison.Ordinal)); var manifest = new StaticWebAssetsPackManifest { Files = [.. packageFiles], ElementsToRemove = [.. AdditionalElementsToRemoveFromPacking.Select(e => e.ItemSpec).OrderBy(id => id)] }; string manifestPath = string.IsNullOrEmpty(ManifestPath) ? ManifestPath : TaskEnvironment.GetAbsolutePath(ManifestPath); PersistManifest(manifest, manifestPath); return !Log.HasLoggedErrors; } private void PersistManifest(StaticWebAssetsPackManifest manifest, string manifestPath) { var data = JsonSerializer.SerializeToUtf8Bytes(manifest, ManifestSerializationOptions); var dataHash = ComputeHash(data); var fileExists = File.Exists(manifestPath); var existingManifestHash = fileExists ? ComputeHash(File.ReadAllBytes(manifestPath)) : ""; if (!fileExists) { Log.LogMessage(MessageImportance.Low, $"Creating manifest because manifest file '{ManifestPath}' does not exist."); File.WriteAllBytes(manifestPath, data); } else if (!string.Equals(dataHash, existingManifestHash, StringComparison.Ordinal)) { Log.LogMessage(MessageImportance.Low, $"Updating manifest because manifest version '{dataHash}' is different from existing manifest hash '{existingManifestHash}'."); File.WriteAllBytes(manifestPath, data); } else { Log.LogMessage(MessageImportance.Low, $"Skipping manifest update because manifest version '{dataHash}' has not changed."); } } private static string ComputeHash(byte[] data) { #if !NET9_0_OR_GREATER using var sha256 = SHA256.Create(); var result = sha256.ComputeHash(data); #else var result = SHA256.HashData(data); #endif return Convert.ToBase64String(result); } private sealed class StaticWebAssetPackageFile { public string Id { get; set; } public string PackagePath { get; set; } } private sealed class StaticWebAssetsPackManifest { public StaticWebAssetPackageFile[] Files { get; set; } public string[] ElementsToRemove { get; set; } } }