File: JSModules\ApplyJsModules.cs
Web Access
Project: ..\..\..\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.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
 
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
public class ApplyJsModules : Task
{
    [Required]
    public ITaskItem[] RazorComponents { get; set; }
 
    [Required]
    public ITaskItem[] RazorGenerate { get; set; }
 
    [Required]
    public ITaskItem[] JSFileModuleCandidates { get; set; }
 
    [Output]
    public ITaskItem[] JsFileModules { get; set; }
 
    public override bool Execute()
    {
        var razorComponentsWithJsModules = new List<ITaskItem>();
        var razorGenerateWithJsModules = new List<ITaskItem>();
        var unmatchedJsModules = new List<ITaskItem>(JSFileModuleCandidates);
        var jsModulesByRazorItem = new Dictionary<string, IList<ITaskItem>>();
 
        for (var i = 0; i < RazorComponents.Length; i++)
        {
            var componentCandidate = RazorComponents[i];
            MatchJsModuleFiles(
                razorComponentsWithJsModules,
                componentCandidate,
                unmatchedJsModules,
                jsModulesByRazorItem,
                "RazorComponent",
                "(.*?)(?:#\\[.*?\\](?:\\?|!)?)?(\\.razor\\.js)$",
                "$1.razor");
        }
 
        for (var i = 0; i < RazorGenerate.Length; i++)
        {
            var razorViewCandidate = RazorGenerate[i];
            MatchJsModuleFiles(
                razorGenerateWithJsModules,
                razorViewCandidate,
                unmatchedJsModules,
                jsModulesByRazorItem,
                "View",
                "(.*?)(?:#\\[.*?\\](?:\\?|!)?)?(\\.cshtml\\.js)$",
                "$1.cshtml");
        }
 
        foreach (var kvp in jsModulesByRazorItem)
        {
            if (RazorComponents.Any(rc => string.Equals(rc.ItemSpec, kvp.Key, StringComparison.OrdinalIgnoreCase)))
            {
                var component = kvp.Key;
                var jsModuleFiles = kvp.Value;
 
                if (jsModuleFiles.Count > 1)
                {
                    Log.LogError(null, "BLAZOR105", "", component, 0, 0, 0, 0, $"More than one JS module files were found for the razor component '{component}'. " +
                        $"Each razor component must have at most a single associated JS module file." +
                        Environment.NewLine +
                        string.Join(Environment.NewLine, jsModuleFiles.Select(f => f.ItemSpec)));
                }
            }
            else
            {
                var view = kvp.Key;
                var jsModuleFiles = kvp.Value;
 
                if (jsModuleFiles.Count > 1)
                {
                    Log.LogError(null, "RZ1007", "", view, 0, 0, 0, 0, $"More than one JS module files were found for the razor view '{view}'. " +
                        $"Each razor view must have at most a single associated JS module file." +
                        Environment.NewLine +
                        string.Join(Environment.NewLine, jsModuleFiles.Select(f => f.ItemSpec)));
                }
            }
        }
 
        foreach (var unmatched in unmatchedJsModules)
        {
            Log.LogError(null, "BLAZOR106", "", unmatched.ItemSpec, 0, 0, 0, 0, $"The JS module file '{unmatched.ItemSpec}' was defined but no associated razor component or view was found for it.");
        }
 
        JsFileModules = jsModulesByRazorItem.Values.SelectMany(e => e).ToArray();
 
        return !Log.HasLoggedErrors;
    }
 
    private static void MatchJsModuleFiles(
        List<ITaskItem> itemsWithScopes,
        ITaskItem itemCandidate,
        List<ITaskItem> unmatchedJsModules,
        Dictionary<string, IList<ITaskItem>> jsModuleByItem,
        string explicitMetadataName,
        string candidateMatchPattern,
        string replacementExpression)
    {
        var i = 0;
        while (i < unmatchedJsModules.Count)
        {
            var jsModuleCandidate = unmatchedJsModules[i];
            var explicitRazorItem = jsModuleCandidate.GetMetadata(explicitMetadataName);
            var jsModuleCandidatePath = jsModuleCandidate.GetMetadata("RelativePath");
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                jsModuleCandidatePath = jsModuleCandidatePath.Replace('/', '\\');
            }
 
            var razorItem = !string.IsNullOrWhiteSpace(explicitRazorItem) ?
                explicitRazorItem :
                Regex.Replace(jsModuleCandidatePath, candidateMatchPattern, replacementExpression, RegexOptions.IgnoreCase);
 
            if (string.Equals(itemCandidate.ItemSpec, razorItem, StringComparison.OrdinalIgnoreCase))
            {
                unmatchedJsModules.RemoveAt(i);
                if (!jsModuleByItem.TryGetValue(itemCandidate.ItemSpec, out var existing))
                {
                    jsModuleByItem[itemCandidate.ItemSpec] = [jsModuleCandidate];
                    var item = new TaskItem(itemCandidate);
                    item.SetMetadata("JSModule", jsModuleCandidate.GetMetadata("JSModule"));
                    itemsWithScopes.Add(item);
                }
                else
                {
                    existing.Add(jsModuleCandidate);
                }
            }
            else
            {
                i++;
            }
        }
    }
}