/src/RazorSdk/Tasks/ApplyCssScopes.cs

https://gitlab.com/dotnetfoundation/sdk · C# · 152 lines · 125 code · 17 blank · 10 comment · 8 complexity · b031699833b91c1eec5624fd66329911 MD5 · raw file

  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Text.RegularExpressions;
  7. using Microsoft.Build.Framework;
  8. using Microsoft.Build.Utilities;
  9. namespace Microsoft.AspNetCore.Razor.Tasks
  10. {
  11. public class ApplyCssScopes : Task
  12. {
  13. [Required]
  14. public ITaskItem[] RazorComponents { get; set; }
  15. [Required]
  16. public ITaskItem[] RazorGenerate { get; set; }
  17. [Required]
  18. public ITaskItem[] ScopedCss { get; set; }
  19. [Output]
  20. public ITaskItem[] RazorComponentsWithScopes { get; set; }
  21. [Output]
  22. public ITaskItem[] RazorGenerateWithScopes { get; set; }
  23. public override bool Execute()
  24. {
  25. var razorComponentsWithScopes = new List<ITaskItem>();
  26. var razorGenerateWithScopes = new List<ITaskItem>();
  27. var unmatchedScopedCss = new List<ITaskItem>(ScopedCss);
  28. var scopedCssByRazorItem = new Dictionary<string, IList<ITaskItem>>();
  29. for (var i = 0; i < RazorComponents.Length; i++)
  30. {
  31. var componentCandidate = RazorComponents[i];
  32. MatchScopedCssFiles(
  33. razorComponentsWithScopes,
  34. componentCandidate,
  35. unmatchedScopedCss,
  36. scopedCssByRazorItem,
  37. "RazorComponent",
  38. "(.*)\\.razor\\.css$",
  39. "$1.razor");
  40. }
  41. for (var i = 0; i < RazorGenerate.Length; i++)
  42. {
  43. var razorViewCandidate = RazorGenerate[i];
  44. MatchScopedCssFiles(
  45. razorGenerateWithScopes,
  46. razorViewCandidate,
  47. unmatchedScopedCss,
  48. scopedCssByRazorItem,
  49. "View",
  50. "(.*)\\.cshtml\\.css$",
  51. "$1.cshtml");
  52. }
  53. foreach (var kvp in scopedCssByRazorItem)
  54. {
  55. if (RazorComponents.Any(rc => string.Equals(rc.ItemSpec, kvp.Key, StringComparison.OrdinalIgnoreCase)))
  56. {
  57. var component = kvp.Key;
  58. var scopeFiles = kvp.Value;
  59. if (scopeFiles.Count > 1)
  60. {
  61. Log.LogError(null, "BLAZOR101", "", component, 0, 0, 0, 0, $"More than one scoped css files were found for the razor component '{component}'. " +
  62. $"Each razor component must have at most a single associated scoped css file." +
  63. Environment.NewLine +
  64. string.Join(Environment.NewLine, scopeFiles.Select(f => f.ItemSpec)));
  65. }
  66. }
  67. else
  68. {
  69. var view = kvp.Key;
  70. var scopeFiles = kvp.Value;
  71. if (scopeFiles.Count > 1)
  72. {
  73. Log.LogError(null, "RZ1007", "", view, 0, 0, 0, 0, $"More than one scoped css files were found for the razor view '{view}'. " +
  74. $"Each razor view must have at most a single associated scoped css file." +
  75. Environment.NewLine +
  76. string.Join(Environment.NewLine, scopeFiles.Select(f => f.ItemSpec)));
  77. }
  78. }
  79. }
  80. // We don't want to allow scoped css files without a matching component. Our convention is very specific in its requirements
  81. // so failing to have a matching component very likely means an error.
  82. // When the matching component was specified explicitly, failing to find a matching component is an error.
  83. // This simplifies a few things like being able to assume that the presence of a .razor.css file or a ScopedCssInput item will result in a bundle being produced,
  84. // that the contents of the bundle are independent of the existence of a component and that users will be able to catch errors at compile
  85. // time instead of wondering why their component doesn't have a scope applied to it.
  86. // In the rare case that a .razor file exists on the user project, has an associated .razor.css file and the user decides to exclude it as a RazorComponent they
  87. // can update the Content item for the .razor.css file with Scoped=false and we will not consider it.
  88. foreach (var unmatched in unmatchedScopedCss)
  89. {
  90. Log.LogError(null, "BLAZOR102", "", unmatched.ItemSpec, 0, 0, 0, 0, $"The scoped css file '{unmatched.ItemSpec}' was defined but no associated razor component or view was found for it.");
  91. }
  92. RazorComponentsWithScopes = razorComponentsWithScopes.ToArray();
  93. RazorGenerateWithScopes = razorGenerateWithScopes.ToArray();
  94. return !Log.HasLoggedErrors;
  95. }
  96. private static void MatchScopedCssFiles(
  97. List<ITaskItem> itemsWithScopes,
  98. ITaskItem itemCandidate,
  99. List<ITaskItem> unmatchedScopedCss,
  100. Dictionary<string, IList<ITaskItem>> scopedCssByItem,
  101. string explicitMetadataName,
  102. string candidateMatchPattern,
  103. string replacementExpression)
  104. {
  105. var j = 0;
  106. while (j < unmatchedScopedCss.Count)
  107. {
  108. var scopedCssCandidate = unmatchedScopedCss[j];
  109. var explicitRazorItem = scopedCssCandidate.GetMetadata(explicitMetadataName);
  110. var razorItem = !string.IsNullOrWhiteSpace(explicitRazorItem) ?
  111. explicitRazorItem :
  112. Regex.Replace(scopedCssCandidate.ItemSpec, candidateMatchPattern, replacementExpression, RegexOptions.IgnoreCase);
  113. if (string.Equals(itemCandidate.ItemSpec, razorItem, StringComparison.OrdinalIgnoreCase))
  114. {
  115. unmatchedScopedCss.RemoveAt(j);
  116. if (!scopedCssByItem.TryGetValue(itemCandidate.ItemSpec, out var existing))
  117. {
  118. scopedCssByItem[itemCandidate.ItemSpec] = new List<ITaskItem>() { scopedCssCandidate };
  119. var item = new TaskItem(itemCandidate);
  120. item.SetMetadata("CssScope", scopedCssCandidate.GetMetadata("CssScope"));
  121. itemsWithScopes.Add(item);
  122. }
  123. else
  124. {
  125. existing.Add(scopedCssCandidate);
  126. }
  127. }
  128. else
  129. {
  130. j++;
  131. }
  132. }
  133. }
  134. }
  135. }