From d0145ed7e904f4a4963803a474a3cf0ab0438de6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 15 Feb 2024 15:23:01 +0100 Subject: [PATCH] V14: generate dynamic importmap (#15710) * register a new IPackageManifestReader to allow to scan the /umbraco/backoffice path for umbraco packages * add constant * add logic to extract the importmap from umbraco package manifests * add html helper to render an importmap * replace static importmap with new dynamic importmap * update tests and be more specific about scopes * remove recursion from PackageManifestReader.cs * add extra test to validate the importmap * combine all string manipulation to produce an importmap into HtmlHelperBackOfficeExtensions.cs * rename IStaticFileHostGenerator to something reflecting its actual usage, and also fix the file names * use auto properties where applicable * add getter for BackOfficeHash and use to simplify BackofficeAssetsPath * ensure BackOffice is always spelled with capital O * add a way to replace the cachebuster for assets imported through an importmap and ensure magic strings are encapsulated into business logic or constants * Review changes * convert primary constructors to explicit and add comments * convert primary constructor to explicit --------- Co-authored-by: kjac --- .../umbraco/UmbracoBackOffice/Default.cshtml | 142 ++---------------- .../umbraco/UmbracoInstall/Index.cshtml | 142 ++---------------- .../Constants-SystemDirectories.cs | 2 + src/Umbraco.Core/Constants-Web.cs | 5 + .../Manifest/IPackageManifestService.cs | 2 + src/Umbraco.Core/Manifest/PackageManifest.cs | 2 + .../Manifest/PackageManifestImportmap.cs | 13 ++ .../UmbracoBuilder.CoreServices.cs | 3 +- .../AppPluginsPackageManifestReader.cs | 24 +++ .../BackOfficePackageManifestReader.cs | 24 +++ .../Manifest/PackageManifestReader.cs | 25 +-- .../Manifest/PackageManifestService.cs | 19 +++ .../UmbracoBuilderExtensions.cs | 2 +- .../HtmlHelperBackOfficeExtensions.cs | 49 ++++++ .../ApplicationBuilderExtensions.cs | 8 +- .../Hosting/IBackOfficePathGenerator.cs | 27 ++++ .../Hosting/IStaticFileHostGenerator.cs | 12 -- .../Hosting/UmbracoBackOfficePathGenerator.cs | 43 ++++++ .../Hosting/UmbracoStaticFilePathGenerator.cs | 45 ------ .../Manifest/PackageManifestReaderTests.cs | 80 +++++++--- 20 files changed, 312 insertions(+), 357 deletions(-) create mode 100644 src/Umbraco.Core/Manifest/PackageManifestImportmap.cs create mode 100644 src/Umbraco.Infrastructure/Manifest/AppPluginsPackageManifestReader.cs create mode 100644 src/Umbraco.Infrastructure/Manifest/BackOfficePackageManifestReader.cs create mode 100644 src/Umbraco.Web.Common/Hosting/IBackOfficePathGenerator.cs delete mode 100644 src/Umbraco.Web.Common/Hosting/IStaticFileHostGenerator.cs create mode 100644 src/Umbraco.Web.Common/Hosting/UmbracoBackOfficePathGenerator.cs delete mode 100644 src/Umbraco.Web.Common/Hosting/UmbracoStaticFilePathGenerator.cs diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml index 833ce72b66..5d3fdbdea9 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml @@ -1,20 +1,15 @@ @using System.Globalization -@using Microsoft.Extensions.Options -@using Umbraco.Cms.Core.Configuration.Models -@using Umbraco.Cms.Core.Hosting +@using Umbraco.Cms.Core.Manifest +@using Umbraco.Cms.Core.Serialization @using Umbraco.Cms.Web.Common.Hosting @using Umbraco.Extensions -@inject IHostingEnvironment HostingEnvironment -@inject IOptions GlobalSettings -@inject IStaticFilePathGenerator StaticFilePathGenerator +@inject IBackOfficePathGenerator BackOfficePathGenerator +@inject IPackageManifestService PackageManifestService +@inject IJsonSerializer JsonSerializer @{ - var backOfficePath = GlobalSettings.Value.GetBackOfficePath(HostingEnvironment); - var backofficeAssetsPath = StaticFilePathGenerator.BackofficeAssetsPath; -} - -@functions { - private static string ImportMapValue(string alias, string path) => $"\"{alias}\": \"{path}\""; + var backOfficePath = BackOfficePathGenerator.BackOfficePath; + var backOfficeAssetsPath = BackOfficePathGenerator.BackOfficeAssetsPath; } @@ -23,126 +18,13 @@ - + Umbraco - - - - + + + @await Html.BackOfficeImportMapScriptAsync(JsonSerializer, BackOfficePathGenerator, PackageManifestService) + diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml index 833ce72b66..5d3fdbdea9 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml @@ -1,20 +1,15 @@ @using System.Globalization -@using Microsoft.Extensions.Options -@using Umbraco.Cms.Core.Configuration.Models -@using Umbraco.Cms.Core.Hosting +@using Umbraco.Cms.Core.Manifest +@using Umbraco.Cms.Core.Serialization @using Umbraco.Cms.Web.Common.Hosting @using Umbraco.Extensions -@inject IHostingEnvironment HostingEnvironment -@inject IOptions GlobalSettings -@inject IStaticFilePathGenerator StaticFilePathGenerator +@inject IBackOfficePathGenerator BackOfficePathGenerator +@inject IPackageManifestService PackageManifestService +@inject IJsonSerializer JsonSerializer @{ - var backOfficePath = GlobalSettings.Value.GetBackOfficePath(HostingEnvironment); - var backofficeAssetsPath = StaticFilePathGenerator.BackofficeAssetsPath; -} - -@functions { - private static string ImportMapValue(string alias, string path) => $"\"{alias}\": \"{path}\""; + var backOfficePath = BackOfficePathGenerator.BackOfficePath; + var backOfficeAssetsPath = BackOfficePathGenerator.BackOfficeAssetsPath; } @@ -23,126 +18,13 @@ - + Umbraco - - - - + + + @await Html.BackOfficeImportMapScriptAsync(JsonSerializer, BackOfficePathGenerator, PackageManifestService) + diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index d0e4488e0e..e6f9fae9fb 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -43,6 +43,8 @@ public static partial class Constants public const string AppPlugins = "/App_Plugins"; + public const string BackOfficePath = "/umbraco/backoffice"; + public const string PluginIcons = "/backoffice/icons"; public const string MvcViews = "~/Views"; diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index b5f54d3416..3577448237 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -44,6 +44,11 @@ public static partial class Constants /// public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser"; + /// + /// The token used to replace the cache buster hash in web assets. + /// + public const string CacheBusterToken = "%CACHE_BUSTER%"; + public static class Mvc { public const string InstallArea = "UmbracoInstall"; diff --git a/src/Umbraco.Core/Manifest/IPackageManifestService.cs b/src/Umbraco.Core/Manifest/IPackageManifestService.cs index 6588861411..50c6c84a92 100644 --- a/src/Umbraco.Core/Manifest/IPackageManifestService.cs +++ b/src/Umbraco.Core/Manifest/IPackageManifestService.cs @@ -3,4 +3,6 @@ public interface IPackageManifestService { Task> GetPackageManifestsAsync(); + + Task GetPackageManifestImportmapAsync(); } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index ba62334240..85f7ac104f 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -9,4 +9,6 @@ public class PackageManifest public bool AllowTelemetry { get; set; } = true; public required object[] Extensions { get; set; } + + public PackageManifestImportmap? Importmap { get; set; } } diff --git a/src/Umbraco.Core/Manifest/PackageManifestImportmap.cs b/src/Umbraco.Core/Manifest/PackageManifestImportmap.cs new file mode 100644 index 0000000000..aea9c6c12d --- /dev/null +++ b/src/Umbraco.Core/Manifest/PackageManifestImportmap.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Manifest; + +[DataContract(Name = "packageManifestImportmap", Namespace = "")] +public class PackageManifestImportmap +{ + [DataMember(Name = "imports")] + public required Dictionary Imports { get; set; } + + [DataMember(Name = "scopes")] + public Dictionary>? Scopes { get; set; } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 4de064f11e..307f153099 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -135,7 +135,8 @@ public static partial class UmbracoBuilderExtensions // register manifest parser, will be injected in collection builders where needed builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); // register the manifest filter collection builder (collection is empty by default) diff --git a/src/Umbraco.Infrastructure/Manifest/AppPluginsPackageManifestReader.cs b/src/Umbraco.Infrastructure/Manifest/AppPluginsPackageManifestReader.cs new file mode 100644 index 0000000000..1580302408 --- /dev/null +++ b/src/Umbraco.Infrastructure/Manifest/AppPluginsPackageManifestReader.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Infrastructure.Manifest; + +/// +/// Reads package manifests from the directory. +/// +internal sealed class AppPluginsPackageManifestReader : PackageManifestReader +{ + public AppPluginsPackageManifestReader( + IPackageManifestFileProviderFactory packageManifestFileProviderFactory, + IJsonSerializer jsonSerializer, + ILogger logger) + : base( + Constants.SystemDirectories.AppPlugins, + packageManifestFileProviderFactory, + jsonSerializer, + logger) + { + } +} diff --git a/src/Umbraco.Infrastructure/Manifest/BackOfficePackageManifestReader.cs b/src/Umbraco.Infrastructure/Manifest/BackOfficePackageManifestReader.cs new file mode 100644 index 0000000000..eba01e6d36 --- /dev/null +++ b/src/Umbraco.Infrastructure/Manifest/BackOfficePackageManifestReader.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Infrastructure.Manifest; + +/// +/// Reads package manifests from the directory. +/// +internal sealed class BackOfficePackageManifestReader : PackageManifestReader +{ + public BackOfficePackageManifestReader( + IPackageManifestFileProviderFactory packageManifestFileProviderFactory, + IJsonSerializer jsonSerializer, + ILogger logger) + : base( + Constants.SystemDirectories.BackOfficePath, + packageManifestFileProviderFactory, + jsonSerializer, + logger) + { + } +} diff --git a/src/Umbraco.Infrastructure/Manifest/PackageManifestReader.cs b/src/Umbraco.Infrastructure/Manifest/PackageManifestReader.cs index c87f1ebecf..7d4a502a35 100644 --- a/src/Umbraco.Infrastructure/Manifest/PackageManifestReader.cs +++ b/src/Umbraco.Infrastructure/Manifest/PackageManifestReader.cs @@ -1,7 +1,6 @@ using System.Text; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Routing; @@ -10,17 +9,20 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Manifest; -internal sealed class AppPluginsFileProviderPackageManifestReader : IPackageManifestReader +internal class PackageManifestReader : IPackageManifestReader { + private readonly string _appPluginsPath; private readonly IPackageManifestFileProviderFactory _packageManifestFileProviderFactory; private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; + private readonly ILogger _logger; - public AppPluginsFileProviderPackageManifestReader( + public PackageManifestReader( + string appPluginsPath, IPackageManifestFileProviderFactory packageManifestFileProviderFactory, IJsonSerializer jsonSerializer, - ILogger logger) + ILogger logger) { + _appPluginsPath = appPluginsPath; _packageManifestFileProviderFactory = packageManifestFileProviderFactory; _jsonSerializer = jsonSerializer; _logger = logger; @@ -34,23 +36,26 @@ internal sealed class AppPluginsFileProviderPackageManifestReader : IPackageMani throw new ArgumentNullException(nameof(fileProvider)); } - IFileInfo[] files = GetAllPackageManifestFiles(fileProvider, Constants.SystemDirectories.AppPlugins).ToArray(); + IFileInfo[] files = GetAllPackageManifestFiles(fileProvider, _appPluginsPath).ToArray(); return await ParsePackageManifestFiles(files); } private static IEnumerable GetAllPackageManifestFiles(IFileProvider fileProvider, string path) { const string extensionFileName = "umbraco-package.json"; + foreach (IFileInfo fileInfo in fileProvider.GetDirectoryContents(path)) { if (fileInfo.IsDirectory) { + // find all extension package configuration files one level deep var virtualPath = WebPath.Combine(path, fileInfo.Name); - - // find all extension package configuration files recursively - foreach (IFileInfo nested in GetAllPackageManifestFiles(fileProvider, virtualPath)) + IDirectoryContents subDirectoryContents = fileProvider.GetDirectoryContents(virtualPath); + IFileInfo? subManifest = subDirectoryContents + .FirstOrDefault(x => !x.IsDirectory && x.Name.InvariantEquals(extensionFileName)); + if (subManifest != null) { - yield return nested; + yield return subManifest; } } else if (fileInfo.Name.InvariantEquals(extensionFileName)) diff --git a/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs b/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs index 70c691abe9..974a907001 100644 --- a/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs +++ b/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs @@ -36,4 +36,23 @@ internal sealed class PackageManifestService : IPackageManifestService }, _packageManifestSettings.CacheTimeout) ?? Array.Empty(); + + public async Task GetPackageManifestImportmapAsync() + { + IEnumerable packageManifests = await GetPackageManifestsAsync(); + var manifests = packageManifests.Select(x => x.Importmap).WhereNotNull().ToList(); + + var importDict = manifests + .SelectMany(x => x.Imports) + .ToDictionary(x => x.Key, x => x.Value); + var scopesDict = manifests + .SelectMany(x => x.Scopes ?? new Dictionary>()) + .ToDictionary(x => x.Key, x => x.Value); + + return new PackageManifestImportmap + { + Imports = importDict, + Scopes = scopesDict, + }; + } } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 961e98c415..1796b03ff8 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -83,7 +83,7 @@ public static partial class UmbracoBuilderExtensions public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) { - builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddSingleton(); builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs index 12f325f0c9..b46e3efb4b 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs @@ -2,16 +2,63 @@ using System.Text; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Newtonsoft.Json; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.Common.Hosting; namespace Umbraco.Extensions; +/// +/// Extensions for to render scripts for the BackOffice. +/// public static class HtmlHelperBackOfficeExtensions { + /// + /// Outputs a script tag containing the import map for the BackOffice. + /// + /// + /// It will replace the token %CACHE_BUSTER% with the cache buster hash. + /// It will also replace the /umbraco/backoffice path with the correct path for the BackOffice assets. + /// + /// A containing the html content for the BackOffice import map. + public static async Task BackOfficeImportMapScriptAsync( + this IHtmlHelper html, + IJsonSerializer jsonSerializer, + IBackOfficePathGenerator backOfficePathGenerator, + IPackageManifestService packageManifestService) + { + try + { + PackageManifestImportmap packageImports = await packageManifestService.GetPackageManifestImportmapAsync(); + + var sb = new StringBuilder(); + sb.AppendLine(""""); + + // Inject the BackOffice cache buster into the import string to handle BackOffice assets + var importmapScript = sb.ToString() + .Replace(backOfficePathGenerator.BackOfficeVirtualDirectory, backOfficePathGenerator.BackOfficeAssetsPath) + .Replace(Constants.Web.CacheBusterToken, backOfficePathGenerator.BackOfficeCacheBustHash); + + return html.Raw(importmapScript); + } + catch (NotSupportedException ex) + { + throw new NotSupportedException("Failed to serialize the BackOffice import map", ex); + } + catch (Exception ex) + { + throw new Exception("Failed to generate the BackOffice import map", ex); + } + } + /// /// Outputs a script tag containing the bare minimum (non secure) server vars for use with the angular app /// @@ -23,6 +70,7 @@ public static class HtmlHelperBackOfficeExtensions /// authenticated, /// we will load the rest of the server vars after the user is authenticated. /// + [Obsolete("This is deprecated and will be removed in V15")] public static async Task BareMinimumServerVariablesScriptAsync(this IHtmlHelper html, BackOfficeServerVariables backOfficeServerVariables) { @@ -132,6 +180,7 @@ public static class HtmlHelperBackOfficeExtensions return html.Raw(sb.ToString()); } + [Obsolete("This is deprecated and will be removed in V15")] public static async Task AngularValueTinyMceAssetsAsync(this IHtmlHelper html, IRuntimeMinifier runtimeMinifier) { diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 885a6998b8..ada8f64fdf 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -186,17 +186,17 @@ public static class ApplicationBuilderExtensions } /// - /// Configure a virtual path with IApplicationBuilder.UseRewriter for backoffice assets to allow cache-busting using the url + /// Configure a virtual path with IApplicationBuilder.UseRewriter for BackOffice assets to allow cache-busting using the url /// /umbraco/backoffice/!cache-busting-id!/assets/index.js => /umbraco/backoffice/assets/index.js. /// public static IApplicationBuilder UseUmbracoBackOfficeRewrites(this IApplicationBuilder builder) { - IStaticFilePathGenerator staticFilePathGenerator = builder.ApplicationServices.GetRequiredService(); - var backofficeAssetsPath = staticFilePathGenerator.BackofficeAssetsPath.TrimStart("/").EnsureEndsWith("/"); + IBackOfficePathGenerator backOfficePathGenerator = builder.ApplicationServices.GetRequiredService(); + var backOfficeAssetsPath = backOfficePathGenerator.BackOfficeAssetsPath.TrimStart("/").EnsureEndsWith("/"); builder.UseRewriter(new RewriteOptions() // The destination needs to be hardcoded to "/umbraco/backoffice" because this is where they are located in the Umbraco.Cms.StaticAssets RCL - .AddRewrite(@"^" + backofficeAssetsPath + "(.+)", "/umbraco/backoffice/$1", true)); + .AddRewrite(@"^" + backOfficeAssetsPath + "(.+)", "/umbraco/backoffice/$1", true)); return builder; } diff --git a/src/Umbraco.Web.Common/Hosting/IBackOfficePathGenerator.cs b/src/Umbraco.Web.Common/Hosting/IBackOfficePathGenerator.cs new file mode 100644 index 0000000000..afe5481cd1 --- /dev/null +++ b/src/Umbraco.Web.Common/Hosting/IBackOfficePathGenerator.cs @@ -0,0 +1,27 @@ +namespace Umbraco.Cms.Web.Common.Hosting; + +/// +/// Umbraco-specific settings for static files from the Umbraco.Cms.StaticAssets RCL. +/// +public interface IBackOfficePathGenerator +{ + /// + /// Gets the virtual path of the BackOffice. + /// + string BackOfficePath { get; } + + /// + /// Gets the cache bust hash for the BackOffice. + /// + string BackOfficeCacheBustHash { get; } + + /// + /// Gets the virtual directory of the BackOffice. + /// + string BackOfficeVirtualDirectory { get; } + + /// + /// Gets the virtual path of the static assets used in the BackOffice. + /// + string BackOfficeAssetsPath { get; } +} diff --git a/src/Umbraco.Web.Common/Hosting/IStaticFileHostGenerator.cs b/src/Umbraco.Web.Common/Hosting/IStaticFileHostGenerator.cs deleted file mode 100644 index 3cd4b8d3b7..0000000000 --- a/src/Umbraco.Web.Common/Hosting/IStaticFileHostGenerator.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Umbraco.Cms.Web.Common.Hosting; - -/// -/// Umbraco-specific settings for static files from the Umbraco.Cms.StaticAssets RCL. -/// -public interface IStaticFilePathGenerator -{ - /// - /// The virtual path of the static assets used in the Backoffice. - /// - string BackofficeAssetsPath { get; } -} diff --git a/src/Umbraco.Web.Common/Hosting/UmbracoBackOfficePathGenerator.cs b/src/Umbraco.Web.Common/Hosting/UmbracoBackOfficePathGenerator.cs new file mode 100644 index 0000000000..cff31ddc91 --- /dev/null +++ b/src/Umbraco.Web.Common/Hosting/UmbracoBackOfficePathGenerator.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.WebAssets; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Hosting; + +/// +public class UmbracoBackOfficePathGenerator : IBackOfficePathGenerator +{ + private string? _backofficeAssetsPath; + private string? _backOfficeVirtualDirectory; + + public UmbracoBackOfficePathGenerator( + IHostingEnvironment hostingEnvironment, + IUmbracoVersion umbracoVersion, + IRuntimeMinifier runtimeMinifier, + IOptions globalSettings) + { + BackOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment); + BackOfficeCacheBustHash = UrlHelperExtensions.GetCacheBustHash(hostingEnvironment, umbracoVersion, runtimeMinifier); + } + + /// + public string BackOfficePath { get; } + + /// + public string BackOfficeCacheBustHash { get; } + + /// + public string BackOfficeVirtualDirectory => _backOfficeVirtualDirectory ??= BackOfficePath.EnsureEndsWith('/') + "backoffice"; + + /// + /// Gets the virtual path for the Backoffice assets coming from the Umbraco.Cms.StaticAssets RCL. + /// The path will contain a generated SHA1 hash that is based on a number of parameters including the UmbracoVersion and runtime minifier. + /// + /// /umbraco/backoffice/addf120b430021c36c232c99ef8d926aea2acd6b + /// + public string BackOfficeAssetsPath => + _backofficeAssetsPath ??= BackOfficeVirtualDirectory.EnsureEndsWith('/') + BackOfficeCacheBustHash; +} diff --git a/src/Umbraco.Web.Common/Hosting/UmbracoStaticFilePathGenerator.cs b/src/Umbraco.Web.Common/Hosting/UmbracoStaticFilePathGenerator.cs deleted file mode 100644 index 564d7d39b3..0000000000 --- a/src/Umbraco.Web.Common/Hosting/UmbracoStaticFilePathGenerator.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.WebAssets; -using Umbraco.Cms.Web.Common.Hosting; -using Umbraco.Extensions; - -public class UmbracoStaticFilePathGenerator : IStaticFilePathGenerator -{ - private string? _backofficeAssetsPath; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IRuntimeMinifier _runtimeMinifier; - private readonly IOptions _globalSettings; - - public UmbracoStaticFilePathGenerator(IHostingEnvironment hostingEnvironment, IUmbracoVersion umbracoVersion, IRuntimeMinifier runtimeMinifier, IOptions globalSettings) - { - _hostingEnvironment = hostingEnvironment; - _umbracoVersion = umbracoVersion; - _runtimeMinifier = runtimeMinifier; - _globalSettings = globalSettings; - } - - /// - /// Get the virtual path for the Backoffice assets coming from the Umbraco.Cms.StaticAssets RCL. - /// The path will contain a generated SHA1 hash that is based on a number of parameters including the UmbracoVersion and runtime minifier. - /// - /// /umbraco/backoffice/addf120b430021c36c232c99ef8d926aea2acd6b - /// - public string BackofficeAssetsPath - { - get - { - if (_backofficeAssetsPath is null) - { - var umbracoHash = UrlHelperExtensions.GetCacheBustHash(_hostingEnvironment, _umbracoVersion, _runtimeMinifier); - var backOfficePath = _globalSettings.Value.GetBackOfficePath(_hostingEnvironment); - _backofficeAssetsPath = backOfficePath.EnsureEndsWith('/') + "backoffice/" + umbracoHash; - } - - return _backofficeAssetsPath; - } - } -} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestReaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestReaderTests.cs index a4b2586e82..205dcf3efa 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestReaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestReaderTests.cs @@ -16,7 +16,7 @@ public class PackageManifestReaderTests { private IPackageManifestReader _reader; private Mock _rootDirectoryContentsMock; - private Mock> _loggerMock; + private Mock> _loggerMock; private Mock _fileProviderMock; [SetUp] @@ -30,8 +30,8 @@ public class PackageManifestReaderTests var fileProviderFactoryMock = new Mock(); fileProviderFactoryMock.Setup(m => m.Create()).Returns(_fileProviderMock.Object); - _loggerMock = new Mock>(); - _reader = new AppPluginsFileProviderPackageManifestReader(fileProviderFactoryMock.Object, new SystemTextJsonSerializer(), _loggerMock.Object); + _loggerMock = new Mock>(); + _reader = new AppPluginsPackageManifestReader(fileProviderFactoryMock.Object, new SystemTextJsonSerializer(), _loggerMock.Object); } [Test] @@ -49,6 +49,21 @@ public class PackageManifestReaderTests Assert.AreEqual("1.2.3", first.Version); Assert.AreEqual(2, first.Extensions.Count()); Assert.IsTrue(first.Extensions.All(e => e is JsonElement)); + + Assert.NotNull(first.Importmap); + var importmap = first.Importmap; + Assert.AreEqual(1, importmap.Imports.Count()); + Assert.AreEqual("./module/shapes/square.js", importmap.Imports["square"]); + + Assert.NotNull(importmap.Scopes); + Assert.AreEqual(1, importmap.Scopes.Count()); + var scope = importmap.Scopes.First(); + Assert.AreEqual("/modules/customshapes", scope.Key); + Assert.NotNull(scope.Value); + var firstScope = scope.Value.First(); + Assert.NotNull(firstScope); + Assert.AreEqual("square", firstScope.Key); + Assert.AreEqual("https://example.com/modules/shapes/square.js", firstScope.Value); } [Test] @@ -66,21 +81,6 @@ public class PackageManifestReaderTests Assert.AreEqual("Package Two", result.Last().Name); } - [Test] - public async Task Can_Read_PackageManifests_Recursively() - { - var childFolder = CreateDirectoryMock("/my-parent-folder/my-child-folder", CreatePackageManifestFile(DefaultPackageManifestContent("Nested Package"))); - var parentFolder = CreateDirectoryMock("/my-parent-folder", childFolder); - - _rootDirectoryContentsMock - .Setup(f => f.GetEnumerator()) - .Returns(new List { parentFolder }.GetEnumerator()); - - var result = await _reader.ReadPackageManifestsAsync(); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("Nested Package", result.First().Name); - } - [Test] public async Task Can_Skip_Empty_Directories() { @@ -131,9 +131,9 @@ public class PackageManifestReaderTests } [Test] - public async Task Cannot_Read_PackageManifest_Without_Name() + public void Cannot_Read_PackageManifest_Without_Name() { - var content = @"{ + const string content = @"{ ""version"": ""1.2.3"", ""allowTelemetry"": true, ""extensions"": [{ @@ -152,9 +152,9 @@ public class PackageManifestReaderTests } [Test] - public async Task Cannot_Read_PackageManifest_Without_Extensions() + public void Cannot_Read_PackageManifest_Without_Extensions() { - var content = @"{ + const string content = @"{ ""name"": ""Something"", ""version"": ""1.2.3"", ""allowTelemetry"": true @@ -167,9 +167,31 @@ public class PackageManifestReaderTests EnsureLogErrorWasCalled(); } + [Test] + public void Cannot_Read_PackageManifest_Without_Importmap_Imports() + { + const string content = @"{ + ""name"": ""Something"", + ""extensions"": [], + ""importmap"": { + ""scopes"": { + ""/modules/customshapes"": { + ""square"": ""https://example.com/modules/shapes/square.js"" + } + } + } +}"; + _rootDirectoryContentsMock + .Setup(f => f.GetEnumerator()) + .Returns(new List { CreatePackageManifestFile(content) }.GetEnumerator()); + + Assert.ThrowsAsync(() => _reader.ReadPackageManifestsAsync()); + EnsureLogErrorWasCalled(); + } + [TestCase("This is not JSON")] [TestCase(@"{""name"": ""invalid-json"", ""version"": ")] - public async Task Cannot_Read_Invalid_PackageManifest(string content) + public void Cannot_Read_Invalid_PackageManifest(string content) { _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) @@ -239,6 +261,16 @@ public class PackageManifestReaderTests }, { ""type"": ""headerApp"" } - ] + ], + ""importmap"": { + ""imports"": { + ""square"": ""./module/shapes/square.js"" + }, + ""scopes"": { + ""/modules/customshapes"": { + ""square"": ""https://example.com/modules/shapes/square.js"" + } + } + } }".Replace("##NAME##", name); }