|
// 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;
public class StaticWebAssetsGeneratePackManifest : Task
{
// 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 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)]
};
PersistManifest(manifest);
return !Log.HasLoggedErrors;
}
private void PersistManifest(StaticWebAssetsPackManifest manifest)
{
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; }
}
}
|