diff --git a/Moonlight.ApiServer/Http/Controllers/Assets/AssetsController.cs b/Moonlight.ApiServer/Http/Controllers/Assets/AssetsController.cs index efc31ee9..df32f208 100644 --- a/Moonlight.ApiServer/Http/Controllers/Assets/AssetsController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Assets/AssetsController.cs @@ -20,7 +20,6 @@ public class AssetsController : Controller { return new FrontendAssetResponse() { - CssFiles = AssetService.GetCssAssets(), JavascriptFiles = AssetService.GetJavascriptAssets(), }; } diff --git a/Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs new file mode 100644 index 00000000..9ca53575 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs @@ -0,0 +1,24 @@ +using Moonlight.ApiServer.Interfaces.Startup; +using Moonlight.ApiServer.Services; + +namespace Moonlight.ApiServer.Implementations.Startup; + +public class CoreAssetStartup : IAppStartup +{ + private readonly BundleService BundleService; + + public CoreAssetStartup(BundleService bundleService) + { + BundleService = bundleService; + } + + public Task BuildApp(IHostApplicationBuilder builder) + { + BundleService.BundleCss("css/core.min.css"); + + return Task.CompletedTask; + } + + public Task ConfigureApp(IApplicationBuilder app) + => Task.CompletedTask; +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/AssetService.cs b/Moonlight.ApiServer/Services/AssetService.cs index 58499a2b..cd88475a 100644 --- a/Moonlight.ApiServer/Services/AssetService.cs +++ b/Moonlight.ApiServer/Services/AssetService.cs @@ -5,7 +5,6 @@ namespace Moonlight.ApiServer.Services; [Singleton] public class AssetService { - public string[] CssFiles { get; private set; } public string[] JavascriptFiles { get; private set; } private bool HasBeenCollected = false; @@ -22,14 +21,6 @@ public class AssetService public void CollectAssets() { - // CSS - var cssFiles = new List(); - - cssFiles.AddRange(AdditionalCssAssets); - cssFiles.AddRange(PluginService.AssetMap.Keys.Where(x => x.EndsWith(".css"))); - - CssFiles = cssFiles.ToArray(); - // Javascript var jsFiles = new List(); @@ -38,23 +29,10 @@ public class AssetService JavascriptFiles = jsFiles.ToArray(); } - - public void AddCssAsset(string asset) - => AdditionalCssAssets.Add(asset); public void AddJavascriptAsset(string asset) => AdditionalJavascriptAssets.Add(asset); - public string[] GetCssAssets() - { - if (HasBeenCollected) - return CssFiles; - - CollectAssets(); - - return CssFiles; - } - public string[] GetJavascriptAssets() { if (HasBeenCollected) diff --git a/Moonlight.ApiServer/Services/BundleGenerationService.cs b/Moonlight.ApiServer/Services/BundleGenerationService.cs new file mode 100644 index 00000000..4311d74b --- /dev/null +++ b/Moonlight.ApiServer/Services/BundleGenerationService.cs @@ -0,0 +1,140 @@ +using ExCSS; +using Microsoft.Extensions.FileProviders; +using MoonCore.Helpers; + +namespace Moonlight.ApiServer.Services; + +public class BundleGenerationService : IHostedService +{ + private readonly ILogger Logger; + private readonly IWebHostEnvironment HostEnvironment; + private readonly BundleService BundleService; + + public BundleGenerationService( + ILogger logger, + IWebHostEnvironment hostEnvironment, + BundleService bundleService + ) + { + Logger = logger; + HostEnvironment = hostEnvironment; + BundleService = bundleService; + } + + private async Task Bundle(CancellationToken cancellationToken) + { + Logger.LogInformation("Bundling css files..."); + + // Search the physical path for the defined files + var physicalCssFiles = new List(); + + foreach (var cssFile in BundleService.GetCssFiles()) + { + var fileInfo = HostEnvironment.WebRootFileProvider.GetFileInfo(cssFile); + + if (fileInfo is NotFoundFileInfo || fileInfo.PhysicalPath == null) + { + Logger.LogWarning( + "Unable to find physical path for the requested css file '{file}'. Make sure its inside a wwwroot folder", + cssFile + ); + + continue; + } + + physicalCssFiles.Add(fileInfo.PhysicalPath); + } + + // TODO: Implement cache + + var bundleContent = await CreateCssBundle(physicalCssFiles); + + Directory.CreateDirectory(PathBuilder.Dir("storage", "tmp")); + await File.WriteAllTextAsync(PathBuilder.File("storage", "tmp", "bundle.css"), bundleContent, + cancellationToken); + + Logger.LogInformation("Successfully built css bundle"); + } + + private async Task CreateCssBundle(List physicalPaths) + { + if (physicalPaths.Count == 0) // No stylesheets defined => nothing to process + return ""; + + if (physicalPaths.Count == 1) // Only one stylesheet => nothing to process + return await File.ReadAllTextAsync(physicalPaths[0]); + + // Create bundle by stripping out double declared classes and combining all css files into one bundle + var content = ""; + var styleSheets = new List(); + var parser = new StylesheetParser(); + + foreach (var fileContent in physicalPaths) + { + try + { + var sh = await parser.ParseAsync(fileContent); + styleSheets.Add(sh); + } + catch (Exception e) + { + Logger.LogWarning("An error occured while parsing css file: {e}", e); + } + } + + // Delegate the first stylesheet as the main stylesheet + var mainStylesheet = styleSheets.First(); + + foreach (var stylesheet in styleSheets.Skip(1)) // Process all stylesheets expect the first (main) one + { + // Style + foreach (var styleRule in stylesheet.StyleRules) + { + if (mainStylesheet.StyleRules.Any(x => x.Selector.Text == styleRule.Selector.Text)) + continue; + + content += styleRule.ToCss() + "\n"; + } + + // Container + foreach (var containerRule in stylesheet.ContainerRules) + { + if (mainStylesheet.ContainerRules.Any(x => x.ConditionText == containerRule.ConditionText)) + continue; + + content += containerRule.ToCss() + "\n"; + } + + // Import Rule + foreach (var importRule in stylesheet.ImportRules) + { + if (mainStylesheet.ImportRules.Any(x => x.Text == importRule.Text)) + continue; + + content += importRule.ToCss() + "\n"; + } + + // Media Rules + foreach (var mediaRule in stylesheet.MediaRules) + content += mediaRule.StylesheetText.Text + "\n"; + + // Page Rules + foreach (var pageRule in stylesheet.PageRules) + { + if (mainStylesheet.PageRules.Any(x => x.SelectorText == pageRule.SelectorText)) + continue; + + content += pageRule.ToCss() + "\n"; + } + } + + return content; + } + + // + public Task StartAsync(CancellationToken cancellationToken) + => Bundle(cancellationToken); + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/BundleService.cs b/Moonlight.ApiServer/Services/BundleService.cs index aa1d4892..28aa6507 100644 --- a/Moonlight.ApiServer/Services/BundleService.cs +++ b/Moonlight.ApiServer/Services/BundleService.cs @@ -1,132 +1,11 @@ -using ExCSS; -using Microsoft.Extensions.Hosting.Internal; -using MoonCore.Helpers; +namespace Moonlight.ApiServer.Services; -namespace Moonlight.ApiServer.Services; - -public class BundleService : IHostedService +public class BundleService { - private readonly ILogger Logger; private readonly List CssFiles = new(); - private readonly AssetService AssetService; - private readonly IWebHostEnvironment HostEnvironment; + public void BundleCss(string path) + => CssFiles.Add(path); - public BundleService(ILogger logger, PluginService pluginService, IWebHostEnvironment hostEnvironment, AssetService assetService) - { - Logger = logger; - HostEnvironment = hostEnvironment; - AssetService = assetService; - } - - public async Task Bundle(CancellationToken cancellationToken) - { - Logger.LogInformation("Collecting css files..."); - - var fi = HostEnvironment.WebRootFileProvider.GetFileInfo("css/core.min.css"); - - CssFiles.Add(fi.PhysicalPath!); - CssFiles.AddRange(AssetService.GetCssAssets().Select(webPath => - { - var fii = HostEnvironment.WebRootFileProvider.GetFileInfo(webPath.TrimStart('/')); - - return fii.PhysicalPath!; - })); - - Logger.LogInformation("Bundling css files..."); - - var content = ""; - - if (CssFiles.Count > 0) - { - var cssFileContents = new List(); - - foreach (var cssFile in CssFiles) - { - var fileContent = await File.ReadAllTextAsync(cssFile, cancellationToken); - cssFileContents.Add(fileContent); - } - - content = cssFileContents[0]; - - if (cssFileContents.Count > 1) - { - var styleSheets = new List(); - - var parser = new StylesheetParser(); - - foreach (var fileContent in cssFileContents) - { - try - { - var sh = await parser.ParseAsync(fileContent, cancellationToken); - styleSheets.Add(sh); - } - catch (Exception e) - { - Logger.LogWarning("An error occured while parsing css file: {e}", e); - } - } - - var mainStylesheet = styleSheets.First(); - - foreach (var stylesheet in styleSheets.Skip(1)) - { - // Style - foreach (var styleRule in stylesheet.StyleRules) - { - if (mainStylesheet.StyleRules.Any(x => x.Selector.Text == styleRule.Selector.Text)) - continue; - - content += styleRule.ToCss() + "\n"; - } - - // Container - foreach (var containerRule in stylesheet.ContainerRules) - { - if (mainStylesheet.ContainerRules.Any(x => x.ConditionText == containerRule.ConditionText)) - continue; - - content += containerRule.ToCss() + "\n"; - } - - // Import Rule - foreach (var importRule in stylesheet.ImportRules) - { - if (mainStylesheet.ImportRules.Any(x => x.Text == importRule.Text)) - continue; - - content += importRule.ToCss() + "\n"; - } - - // Media Rules - foreach (var mediaRule in stylesheet.MediaRules) - content += mediaRule.StylesheetText.Text + "\n"; - - // Page Rules - foreach (var pageRule in stylesheet.PageRules) - { - if (mainStylesheet.PageRules.Any(x => x.SelectorText == pageRule.SelectorText)) - continue; - - content += pageRule.ToCss() + "\n"; - } - } - } - } - - Directory.CreateDirectory(PathBuilder.Dir("storage", "tmp")); - await File.WriteAllTextAsync(PathBuilder.File("storage", "tmp", "bundle.css"), content, cancellationToken); - - Logger.LogInformation("Successfully built css bundle"); - } - - public void AddCssFile(string file) => CssFiles.Add(file); - - // - public Task StartAsync(CancellationToken cancellationToken) - => Bundle(cancellationToken); - - public Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; + public IEnumerable GetCssFiles() => CssFiles; } \ No newline at end of file diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 767c5826..10cc8306 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -51,6 +51,9 @@ public class Startup // Plugin Loading private PluginService PluginService; private PluginLoaderService PluginLoaderService; + + // Asset bundling + private BundleService BundleService; private IAppStartup[] PluginAppStartups; private IDatabaseStartup[] PluginDatabaseStartups; @@ -150,7 +153,7 @@ public class Startup var assetService = WebApplication.Services.GetRequiredService(); - for (int i = 0; i < Args.Length; i++) + for (var i = 0; i < Args.Length; i++) { var currentArg = Args[i]; @@ -177,7 +180,7 @@ public class Startup switch (extension) { case ".css": - assetService.AddCssAsset(nextArg); + BundleService.BundleCss(nextArg); break; case ".js": assetService.AddJavascriptAsset(nextArg); @@ -316,7 +319,10 @@ public class Startup // Configure base services for initialisation initialisationServiceCollection.AddSingleton(Configuration); - + + BundleService = new BundleService(); + initialisationServiceCollection.AddSingleton(BundleService); + initialisationServiceCollection.AddLogging(builder => { builder.AddProviders(LoggerProviders); }); // Configure plugin loading by using the interface service @@ -344,8 +350,9 @@ public class Startup private Task RegisterPluginAssets() { - WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); - WebApplicationBuilder.Services.AddSingleton(); + WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); + WebApplicationBuilder.Services.AddSingleton(); + WebApplicationBuilder.Services.AddSingleton(BundleService); return Task.CompletedTask; } diff --git a/Moonlight.Client/Startup.cs b/Moonlight.Client/Startup.cs index ef8c22c4..13a403b1 100644 --- a/Moonlight.Client/Startup.cs +++ b/Moonlight.Client/Startup.cs @@ -134,9 +134,6 @@ public class Startup var assetManifest = await apiClient.GetJson("api/assets"); var jsRuntime = WebAssemblyHost.Services.GetRequiredService(); - - foreach (var cssFile in assetManifest.CssFiles) - await jsRuntime.InvokeVoidAsync("moonlight.assets.loadCss", cssFile); foreach (var javascriptFile in assetManifest.JavascriptFiles) await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", javascriptFile); diff --git a/Moonlight.Client/wwwroot/index.html b/Moonlight.Client/wwwroot/index.html index 8d9505b4..d8aa3c97 100644 --- a/Moonlight.Client/wwwroot/index.html +++ b/Moonlight.Client/wwwroot/index.html @@ -6,7 +6,7 @@ Moonlight.Client - + diff --git a/Moonlight.Client/wwwroot/js/moonlight.js b/Moonlight.Client/wwwroot/js/moonlight.js index 480e6186..0fb2d834 100644 --- a/Moonlight.Client/wwwroot/js/moonlight.js +++ b/Moonlight.Client/wwwroot/js/moonlight.js @@ -27,15 +27,6 @@ window.moonlight = { } }, assets: { - loadCss: function (url) { - let linkElement = document.createElement('link'); - - linkElement.href = url; - linkElement.rel = 'stylesheet'; - linkElement.type = 'text/css'; - - (document.head || document.documentElement).appendChild(linkElement); - }, loadJavascript: function (url) { let scriptElement = document.createElement('script'); diff --git a/Moonlight.Shared/Http/Responses/Assets/FrontendAssetResponse.cs b/Moonlight.Shared/Http/Responses/Assets/FrontendAssetResponse.cs index 3892add2..8eb17cb3 100644 --- a/Moonlight.Shared/Http/Responses/Assets/FrontendAssetResponse.cs +++ b/Moonlight.Shared/Http/Responses/Assets/FrontendAssetResponse.cs @@ -2,6 +2,5 @@ namespace Moonlight.Shared.Http.Responses.Assets; public class FrontendAssetResponse { - public string[] CssFiles { get; set; } public string[] JavascriptFiles { get; set; } } \ No newline at end of file