File: Assets\ImportMapDefinition.cs
Web Access
Project: src\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Linq;
using System.Text.Json;
 
namespace Microsoft.AspNetCore.Components;
 
/// <summary>
/// Represents the contents of a <c><script type="importmap"></script></c> element that defines the import map
/// for module scripts in the application.
/// </summary>
/// <remarks>
/// The import map is a JSON object that defines the mapping of module import specifiers to URLs.
/// <see cref="ImportMapDefinition"/> instances are expensive to create, so it is recommended to cache them if
/// you are creating an additional instance.
/// </remarks>
public sealed class ImportMapDefinition
{
    private Dictionary<string, string>? _imports;
    private Dictionary<string, IReadOnlyDictionary<string, string>>? _scopes;
    private Dictionary<string, string>? _integrity;
    private string? _json;
 
    /// <summary>
    /// Initializes a new instance of <see cref="ImportMapDefinition"/>."/> with the specified imports, scopes, and integrity.
    /// </summary>
    /// <param name="imports">The unscoped imports defined in the import map.</param>
    /// <param name="scopes">The scoped imports defined in the import map.</param>
    /// <param name="integrity">The integrity for the imports defined in the import map.</param>
    /// <remarks>
    /// The <paramref name="imports"/>, <paramref name="scopes"/>, and <paramref name="integrity"/> parameters
    /// will be copied into the new instance. The original collections will not be modified.
    /// </remarks>
    public ImportMapDefinition(
        IReadOnlyDictionary<string, string>? imports,
        IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>>? scopes,
        IReadOnlyDictionary<string, string>? integrity)
    {
        _imports = imports?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        _integrity = integrity?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        _scopes = scopes?.ToDictionary(
            kvp => kvp.Key,
            kvp => kvp.Value.ToDictionary(scopeKvp => scopeKvp.Key, scopeKvp => scopeKvp.Value) as IReadOnlyDictionary<string, string>);
    }
 
    private ImportMapDefinition()
    {
    }
 
    /// <summary>
    /// Creates an import map from a <see cref="ResourceAssetCollection"/>.
    /// </summary>
    /// <param name="assets">The collection of assets to create the import map from.</param>
    /// <returns>The import map.</returns>
    public static ImportMapDefinition FromResourceCollection(ResourceAssetCollection assets)
    {
        var importMap = new ImportMapDefinition();
        foreach (var asset in assets)
        {
            if (!(asset.Url.EndsWith(".mjs", StringComparison.OrdinalIgnoreCase) ||
                asset.Url.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) ||
                asset.Properties == null)
            {
                continue;
            }
 
            var (integrity, label) = GetAssetProperties(asset);
            if (integrity != null)
            {
                importMap._integrity ??= [];
                importMap._integrity[$"./{asset.Url}"] = integrity;
            }
 
            if (label != null)
            {
                importMap._imports ??= [];
                importMap._imports[$"./{label}"] = $"./{asset.Url}";
            }
        }
 
        return importMap;
    }
 
    private static (string? integrity, string? label) GetAssetProperties(ResourceAsset asset)
    {
        string? integrity = null;
        string? label = null;
        for (var i = 0; i < asset.Properties!.Count; i++)
        {
            var property = asset.Properties[i];
            if (string.Equals(property.Name, "integrity", StringComparison.OrdinalIgnoreCase))
            {
                integrity = property.Value;
            }
            else if (string.Equals(property.Name, "label", StringComparison.OrdinalIgnoreCase))
            {
                label = property.Value;
            }
 
            if (integrity != null && label != null)
            {
                return (integrity, label);
            }
        }
 
        return (integrity, label);
    }
 
    /// <summary>
    /// Combines one or more import maps into a single import map.
    /// </summary>
    /// <param name="sources">The list of import maps to combine.</param>
    /// <returns>
    /// A new import map that is the combination of all the input import maps with their
    /// entries applied in order.
    /// </returns>
    public static ImportMapDefinition Combine(params ImportMapDefinition[] sources)
    {
        var importMap = new ImportMapDefinition();
        foreach (var item in sources)
        {
            if (item.Imports != null)
            {
                importMap._imports ??= [];
                foreach (var (key, value) in item.Imports)
                {
                    importMap._imports[key] = value;
                }
            }
 
            if (item.Scopes != null)
            {
                importMap._scopes ??= [];
                foreach (var (key, value) in item.Scopes)
                {
                    if (importMap._scopes.TryGetValue(key, out var existingScope) && existingScope != null)
                    {
                        foreach (var (scopeKey, scopeValue) in value)
                        {
                            ((Dictionary<string, string>)importMap._scopes[key])[scopeKey] = scopeValue;
                        }
                    }
                    else
                    {
                        importMap._scopes[key] = new Dictionary<string, string>(value);
                    }
                }
            }
 
            if (item.Integrity != null)
            {
                importMap._integrity ??= [];
                foreach (var (key, value) in item.Integrity)
                {
                    importMap._integrity[key] = value;
                }
            }
        }
 
        return importMap;
    }
 
    // Example:
    // "imports": {
    //   "triangle": "./module/shapes/triangle.js",
    //   "pentagram": "https://example.com/shapes/pentagram.js"
    // }
    /// <summary>
    /// Gets the unscoped imports defined in the import map.
    /// </summary>
    public IReadOnlyDictionary<string, string>? Imports { get => _imports; }
 
    // Example:
    // {
    //   "imports": {
    //     "triangle": "./module/shapes/triangle.js"
    //   },
    //   "scopes": {
    //     "/modules/myshapes/": {
    //       "triangle": "https://example.com/modules/myshapes/triangle.js"
    //     }
    //   }
    // }
    /// <summary>
    /// Gets the scoped imports defined in the import map.
    /// </summary>
    public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>>? Scopes { get => _scopes; }
 
    // Example:
    // <script type="importmap">
    // {
    //   "imports": {
    //     "triangle": "./module/shapes/triangle.js"
    //   },
    //   "integrity": {
    //     "./module/shapes/triangle.js": "sha256-..."
    //   }
    // }
    // </script>
    /// <summary>
    /// Gets the integrity properties defined in the import map.
    /// </summary>
    public IReadOnlyDictionary<string, string>? Integrity { get => _integrity; }
 
    internal string ToJson()
    {
        _json ??= JsonSerializer.Serialize(this, ImportMapSerializerContext.CustomEncoder.Options);
        return _json;
    }
 
    /// <inheritdoc />
    public override string ToString() => ToJson();
}