Added css bundle api. Improved css bundling code

I made the code cleaner as requested @Masu-Baumgartner  :>
This commit is contained in:
2024-12-10 21:25:46 +01:00
parent 75cefea4fa
commit e63a3db8b9
10 changed files with 182 additions and 168 deletions

View File

@@ -20,7 +20,6 @@ public class AssetsController : Controller
{
return new FrontendAssetResponse()
{
CssFiles = AssetService.GetCssAssets(),
JavascriptFiles = AssetService.GetJavascriptAssets(),
};
}

View File

@@ -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;
}

View File

@@ -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<string>();
cssFiles.AddRange(AdditionalCssAssets);
cssFiles.AddRange(PluginService.AssetMap.Keys.Where(x => x.EndsWith(".css")));
CssFiles = cssFiles.ToArray();
// Javascript
var jsFiles = new List<string>();
@@ -39,22 +30,9 @@ 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)

View File

@@ -0,0 +1,140 @@
using ExCSS;
using Microsoft.Extensions.FileProviders;
using MoonCore.Helpers;
namespace Moonlight.ApiServer.Services;
public class BundleGenerationService : IHostedService
{
private readonly ILogger<BundleGenerationService> Logger;
private readonly IWebHostEnvironment HostEnvironment;
private readonly BundleService BundleService;
public BundleGenerationService(
ILogger<BundleGenerationService> 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<string>();
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<string> CreateCssBundle(List<string> 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<Stylesheet>();
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;
}

View File

@@ -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<BundleService> Logger;
private readonly List<string> CssFiles = new();
private readonly AssetService AssetService;
private readonly IWebHostEnvironment HostEnvironment;
public void BundleCss(string path)
=> CssFiles.Add(path);
public BundleService(ILogger<BundleService> 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<string>();
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<Stylesheet>();
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<string> GetCssFiles() => CssFiles;
}

View File

@@ -52,6 +52,9 @@ public class Startup
private PluginService PluginService;
private PluginLoaderService PluginLoaderService;
// Asset bundling
private BundleService BundleService;
private IAppStartup[] PluginAppStartups;
private IDatabaseStartup[] PluginDatabaseStartups;
private IEndpointStartup[] PluginEndpointStartups;
@@ -150,7 +153,7 @@ public class Startup
var assetService = WebApplication.Services.GetRequiredService<AssetService>();
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);
@@ -317,6 +320,9 @@ 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<BundleService>());
WebApplicationBuilder.Services.AddSingleton<BundleService>();
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<BundleGenerationService>());
WebApplicationBuilder.Services.AddSingleton<BundleGenerationService>();
WebApplicationBuilder.Services.AddSingleton(BundleService);
return Task.CompletedTask;
}

View File

@@ -135,9 +135,6 @@ public class Startup
var jsRuntime = WebAssemblyHost.Services.GetRequiredService<IJSRuntime>();
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);
}

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonlight.Client</title>
<base href="/" />
<link rel="stylesheet" href="/css/core.min.css" />
<link rel="stylesheet" href="/css/bundle.css" />
<link href="manifest.webmanifest" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />

View File

@@ -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');

View File

@@ -2,6 +2,5 @@ namespace Moonlight.Shared.Http.Responses.Assets;
public class FrontendAssetResponse
{
public string[] CssFiles { get; set; }
public string[] JavascriptFiles { get; set; }
}