Files
Umbraco-CMS/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs
Jacob Overgaard 954d3ecc86 v10: add Umbraco UI Library to the backoffice (#13031)
Co-authored-by: Warren Buckley <warren@umbraco.com>
2022-09-20 09:58:59 +02:00

325 lines
13 KiB
C#

using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.WebAssets;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.WebAssets;
public class BackOfficeWebAssets
{
public const string UmbracoPreviewJsBundleName = "umbraco-preview-js";
public const string UmbracoPreviewCssBundleName = "umbraco-preview-css";
public const string UmbracoCssBundleName = "umbraco-backoffice-css";
public const string UmbracoInitCssBundleName = "umbraco-backoffice-init-css";
public const string UmbracoCoreJsBundleName = "umbraco-backoffice-js";
public const string UmbracoExtensionsJsBundleName = "umbraco-backoffice-extensions-js";
public const string UmbracoNonOptimizedPackageJsBundleName = "umbraco-backoffice-non-optimized-js";
public const string UmbracoNonOptimizedPackageCssBundleName = "umbraco-backoffice-non-optimized-css";
public const string UmbracoTinyMceJsBundleName = "umbraco-tinymce-js";
public const string UmbracoUpgradeCssBundleName = "umbraco-authorize-upgrade-css";
private readonly CustomBackOfficeAssetsCollection _customBackOfficeAssetsCollection;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IManifestParser _parser;
private readonly PropertyEditorCollection _propertyEditorCollection;
private readonly IRuntimeMinifier _runtimeMinifier;
private GlobalSettings _globalSettings;
public BackOfficeWebAssets(
IRuntimeMinifier runtimeMinifier,
IManifestParser parser,
PropertyEditorCollection propertyEditorCollection,
IHostingEnvironment hostingEnvironment,
IOptionsMonitor<GlobalSettings> globalSettings,
CustomBackOfficeAssetsCollection customBackOfficeAssetsCollection)
{
_runtimeMinifier = runtimeMinifier;
_parser = parser;
_propertyEditorCollection = propertyEditorCollection;
_hostingEnvironment = hostingEnvironment;
_globalSettings = globalSettings.CurrentValue;
_customBackOfficeAssetsCollection = customBackOfficeAssetsCollection;
globalSettings.OnChange(x => _globalSettings = x);
}
public static string GetIndependentPackageBundleName(ManifestAssets manifestAssets, AssetType assetType)
=> $"{manifestAssets.PackageName.ToLowerInvariant()}-{(assetType == AssetType.Css ? "css" : "js")}";
public void CreateBundles()
{
// Create bundles
_runtimeMinifier.CreateCssBundle(
UmbracoInitCssBundleName,
BundlingOptions.NotOptimizedAndComposite,
FormatPaths(
"assets/css/umbraco.min.css",
"lib/umbraco-ui/uui-css/dist/custom-properties.css",
"lib/umbraco-ui/uui-css/dist/uui-text.css",
"lib/bootstrap-social/bootstrap-social.css",
"lib/font-awesome/css/font-awesome.min.css"));
_runtimeMinifier.CreateCssBundle(
UmbracoUpgradeCssBundleName,
BundlingOptions.NotOptimizedAndComposite,
FormatPaths(
"assets/css/umbraco.min.css",
"lib/bootstrap-social/bootstrap-social.css",
"lib/font-awesome/css/font-awesome.min.css"));
_runtimeMinifier.CreateCssBundle(
UmbracoPreviewCssBundleName,
BundlingOptions.NotOptimizedAndComposite,
FormatPaths("assets/css/canvasdesigner.min.css"));
_runtimeMinifier.CreateJsBundle(
UmbracoPreviewJsBundleName,
BundlingOptions.NotOptimizedAndComposite,
FormatPaths(GetScriptsForPreview()));
_runtimeMinifier.CreateJsBundle(
UmbracoTinyMceJsBundleName,
BundlingOptions.NotOptimizedAndComposite,
FormatPaths(GetScriptsForTinyMce()));
_runtimeMinifier.CreateJsBundle(
UmbracoCoreJsBundleName,
BundlingOptions.NotOptimizedAndComposite,
FormatPaths(GetScriptsForBackOfficeCore()));
// get the property editor assets
var propertyEditorAssets = ScanPropertyEditors()
.GroupBy(x => x.AssetType)
.ToDictionary(x => x.Key, x => x.Select(c => c.FilePath));
// get the back office custom assets
var customAssets = _customBackOfficeAssetsCollection.GroupBy(x => x.DependencyType)
.ToDictionary(x => x.Key, x => x.Select(c => c.FilePath));
// This bundle includes all scripts from property editor assets,
// custom back office assets, and any scripts found in package manifests
// that have the default bundle options.
IEnumerable<string?> jsAssets =
(customAssets.TryGetValue(AssetType.Javascript, out IEnumerable<string?>? customScripts)
? customScripts
: Enumerable.Empty<string>())
.Union(propertyEditorAssets.TryGetValue(AssetType.Javascript, out IEnumerable<string>? scripts)
? scripts
: Enumerable.Empty<string>());
_runtimeMinifier.CreateJsBundle(
UmbracoExtensionsJsBundleName,
BundlingOptions.OptimizedAndComposite,
FormatPaths(
GetScriptsForBackOfficeExtensions(jsAssets)));
// Create a bundle per package manifest that is declaring an Independent bundle type
RegisterPackageBundlesForIndependentOptions(_parser.CombinedManifest.Scripts, AssetType.Javascript);
// Create a single non-optimized (no file processing) bundle for all manifests declaring None as a bundle option
RegisterPackageBundlesForNoneOption(_parser.CombinedManifest.Scripts, UmbracoNonOptimizedPackageJsBundleName);
// This bundle includes all CSS from property editor assets,
// custom back office assets, and any CSS found in package manifests
// that have the default bundle options.
IEnumerable<string?> cssAssets =
(customAssets.TryGetValue(AssetType.Css, out IEnumerable<string?>? customStyles)
? customStyles
: Enumerable.Empty<string>())
.Union(propertyEditorAssets.TryGetValue(AssetType.Css, out IEnumerable<string>? styles)
? styles
: Enumerable.Empty<string>());
_runtimeMinifier.CreateCssBundle(
UmbracoCssBundleName,
BundlingOptions.OptimizedAndComposite,
FormatPaths(
GetStylesheetsForBackOffice(cssAssets)));
// Create a bundle per package manifest that is declaring an Independent bundle type
RegisterPackageBundlesForIndependentOptions(_parser.CombinedManifest?.Stylesheets, AssetType.Css);
// Create a single non-optimized (no file processing) bundle for all manifests declaring None as a bundle option
RegisterPackageBundlesForNoneOption(
_parser.CombinedManifest?.Stylesheets,
UmbracoNonOptimizedPackageCssBundleName);
}
private void RegisterPackageBundlesForNoneOption(
IReadOnlyDictionary<BundleOptions, IReadOnlyList<ManifestAssets>>? combinedPackageManifestAssets,
string bundleName)
{
var assets = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
// Create a bundle per package manifest that is declaring the matching BundleOptions
if (combinedPackageManifestAssets?.TryGetValue(
BundleOptions.None,
out IReadOnlyList<ManifestAssets>? manifestAssetList) ?? false)
{
foreach (var asset in manifestAssetList.SelectMany(x => x.Assets))
{
assets.Add(asset);
}
}
_runtimeMinifier.CreateJsBundle(
bundleName,
// no optimization, no composite files, just render individual files
BundlingOptions.NotOptimizedNotComposite,
FormatPaths(assets.ToArray()));
}
private void RegisterPackageBundlesForIndependentOptions(
IReadOnlyDictionary<BundleOptions, IReadOnlyList<ManifestAssets>>? combinedPackageManifestAssets,
AssetType assetType)
{
// Create a bundle per package manifest that is declaring the matching BundleOptions
if (combinedPackageManifestAssets?.TryGetValue(
BundleOptions.Independent,
out IReadOnlyList<ManifestAssets>? manifestAssetList) ?? false)
{
foreach (ManifestAssets manifestAssets in manifestAssetList)
{
var bundleName = GetIndependentPackageBundleName(manifestAssets, assetType);
var filePaths = FormatPaths(manifestAssets.Assets.ToArray());
switch (assetType)
{
case AssetType.Javascript:
_runtimeMinifier.CreateJsBundle(bundleName, BundlingOptions.OptimizedAndComposite, filePaths);
break;
case AssetType.Css:
_runtimeMinifier.CreateCssBundle(bundleName, BundlingOptions.OptimizedAndComposite, filePaths);
break;
default:
throw new IndexOutOfRangeException();
}
}
}
}
/// <summary>
/// Returns scripts used to load the back office
/// </summary>
/// <returns></returns>
private string[] GetScriptsForBackOfficeExtensions(IEnumerable<string?> propertyEditorScripts)
{
var scripts = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
// only include scripts with the default bundle options here
if (_parser.CombinedManifest.Scripts.TryGetValue(
BundleOptions.Default,
out IReadOnlyList<ManifestAssets>? manifestAssets))
{
foreach (var script in manifestAssets.SelectMany(x => x.Assets))
{
scripts.Add(script);
}
}
foreach (var script in propertyEditorScripts)
{
if (script is not null)
{
scripts.Add(script);
}
}
return scripts.ToArray();
}
/// <summary>
/// Returns the list of scripts for back office initialization
/// </summary>
/// <returns></returns>
private string[]? GetScriptsForBackOfficeCore()
{
JArray? resources = JsonConvert.DeserializeObject<JArray>(Resources.JsInitialize);
return resources?.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()).ToArray();
}
/// <summary>
/// Returns stylesheets used to load the back office
/// </summary>
/// <returns></returns>
private string[] GetStylesheetsForBackOffice(IEnumerable<string?> propertyEditorStyles)
{
var stylesheets = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
// only include css with the default bundle options here
if (_parser.CombinedManifest.Stylesheets.TryGetValue(
BundleOptions.Default,
out IReadOnlyList<ManifestAssets>? manifestAssets))
{
foreach (var script in manifestAssets.SelectMany(x => x.Assets))
{
stylesheets.Add(script);
}
}
foreach (var stylesheet in propertyEditorStyles)
{
if (stylesheet is not null)
{
stylesheets.Add(stylesheet);
}
}
return stylesheets.ToArray();
}
/// <summary>
/// Returns the scripts used for tinymce
/// </summary>
/// <returns></returns>
private string[]? GetScriptsForTinyMce()
{
JArray? resources = JsonConvert.DeserializeObject<JArray>(Resources.TinyMceInitialize);
return resources?.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()).ToArray();
}
/// <summary>
/// Returns the scripts used for preview
/// </summary>
/// <returns></returns>
private string[]? GetScriptsForPreview()
{
JArray? resources = JsonConvert.DeserializeObject<JArray>(Resources.PreviewInitialize);
return resources?.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()).ToArray();
}
/// <summary>
/// Re-format asset paths to be absolute paths
/// </summary>
/// <param name="assets"></param>
/// <returns></returns>
private string[]? FormatPaths(params string[]? assets)
{
var umbracoPath = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment);
return assets?
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(x => !x.StartsWith("/") && Uri.IsWellFormedUriString(x, UriKind.Relative)
// most declarations with be made relative to the /umbraco folder, so things
// like lib/blah/blah.js so we need to turn them into absolutes here
? umbracoPath.EnsureStartsWith('/').TrimEnd("/") + x.EnsureStartsWith('/')
: x).ToArray();
}
/// <summary>
/// Returns the web asset paths to load for property editors that have the <see cref="PropertyEditorAssetAttribute" />
/// attribute applied
/// </summary>
/// <returns></returns>
private IEnumerable<PropertyEditorAssetAttribute> ScanPropertyEditors() =>
_propertyEditorCollection
.SelectMany(x => x.GetType().GetCustomAttributes<PropertyEditorAssetAttribute>(false));
}