File: ComponentManifest.cs
Web Access
Project: src\tools\GenerateTemplateManifest\GenerateTemplateManifest.csproj (GenerateTemplateManifest)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Xml.Linq;
using NuGet.Versioning;
 
namespace GenerateTemplateManifest;
 
internal sealed class ComponentManifest(IReadOnlySet<string> builtPackages)
{
    private readonly SortedDictionary<string, JsonObject> _registrations = new(StringComparer.Ordinal);
 
    internal int Count => _registrations.Count;
 
    internal void RegisterNuGetAssets(JsonElement assets, string source)
    {
        foreach (var library in assets.GetProperty("libraries").EnumerateObject())
        {
            if (library.Value.GetProperty("type").GetString() == "package")
            {
                // Assets library keys are "<package-id>/<resolved-version>", not version ranges.
                var separator = library.Name.LastIndexOf('/');
                if (separator <= 0 || separator == library.Name.Length - 1)
                {
                    throw new InvalidDataException($"Invalid NuGet library '{library.Name}' in {source}");
                }
                Register("nuget", library.Name[..separator], library.Name[(separator + 1)..], source);
            }
        }
 
        foreach (var framework in assets.GetProperty("project").GetProperty("frameworks").EnumerateObject())
        {
            if (!framework.Value.TryGetProperty("downloadDependencies", out var downloads))
            {
                continue;
            }
            foreach (var download in downloads.EnumerateArray())
            {
                // PackageDownload entries live outside "libraries", e.g.
                // "downloadDependencies": [{"name":"Example","version":"[1.2.3, 1.2.3]"}].
                // Multiple exact downloads use "[1.2.3, 1.2.3];[2.0.0, 2.0.0]".
                var name = download.GetProperty("name").GetString() ?? "";
                var version = download.GetProperty("version").GetString() ?? "";
                foreach (var entry in version.Split(';'))
                {
                    if (!VersionRange.TryParse(entry, out var range) ||
                        range.IsFloating || !range.IsMinInclusive || !range.IsMaxInclusive ||
                        range.MinVersion is null || range.MaxVersion is null || range.MinVersion != range.MaxVersion)
                    {
                        throw new InvalidDataException($"Unsupported PackageDownload version '{version}' for '{name}' in {source}; an exact version is required.");
                    }
                    Register("nuget", name, range.MinVersion.ToNormalizedString(), source);
                }
            }
        }
    }
 
    internal void Register(string type, string name, string version, string source)
    {
        if (string.IsNullOrWhiteSpace(name) || !NuGetVersion.TryParse(version, out var parsedVersion))
        {
            throw new InvalidDataException($"Invalid {type} package '{name}' version '{version}' in {source}");
        }
        if (type == "nuget")
        {
            name = name.ToLowerInvariant();
            version = parsedVersion.ToNormalizedString().ToLowerInvariant();
            if (builtPackages.Contains($"{name}/{version}"))
            {
                return;
            }
        }
        _registrations[$"{type}/{name}/{version}"] = new JsonObject
        {
            ["component"] = new JsonObject
            {
                ["type"] = type,
                [type] = new JsonObject { ["name"] = name, ["version"] = version }
            }
        };
    }
 
    internal string Serialize()
    {
        var entries = new JsonArray();
        foreach (var registration in _registrations.Values)
        {
            entries.Add(registration.DeepClone());
        }
        var manifest = new JsonObject
        {
            ["$schema"] = "https://json.schemastore.org/component-detection-manifest.json",
            ["version"] = 1,
            ["registrations"] = entries
        };
        return manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }).ReplaceLineEndings("\n") + "\n";
    }
 
    internal static HashSet<string> ReadBuiltPackages(string directory)
    {
        if (!Directory.Exists(directory))
        {
            throw new DirectoryNotFoundException($"Shipping packages not found: {directory}. Build with -pack before generating the template manifest.");
        }
        var packages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var path in Directory.EnumerateFiles(directory, "*.nupkg"))
        {
            using var archive = ZipFile.OpenRead(path);
            var nuspec = archive.Entries.Single(entry => entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase));
            using var stream = nuspec.Open();
            var document = XDocument.Load(stream);
            var ns = document.Root!.Name.Namespace;
            var metadata = document.Root.Element(ns + "metadata")!;
            var id = metadata.Element(ns + "id")!.Value;
            var version = NuGetVersion.Parse(metadata.Element(ns + "version")!.Value).ToNormalizedString();
            packages.Add($"{id}/{version}");
        }
        if (packages.Count == 0)
        {
            throw new InvalidDataException($"No shipping NuGet packages found in {directory}.");
        }
        return packages;
    }
 
}