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() return new FrontendAssetResponse()
{ {
CssFiles = AssetService.GetCssAssets(),
JavascriptFiles = AssetService.GetJavascriptAssets(), 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] [Singleton]
public class AssetService public class AssetService
{ {
public string[] CssFiles { get; private set; }
public string[] JavascriptFiles { get; private set; } public string[] JavascriptFiles { get; private set; }
private bool HasBeenCollected = false; private bool HasBeenCollected = false;
@@ -22,14 +21,6 @@ public class AssetService
public void CollectAssets() 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 // Javascript
var jsFiles = new List<string>(); var jsFiles = new List<string>();
@@ -38,23 +29,10 @@ public class AssetService
JavascriptFiles = jsFiles.ToArray(); JavascriptFiles = jsFiles.ToArray();
} }
public void AddCssAsset(string asset)
=> AdditionalCssAssets.Add(asset);
public void AddJavascriptAsset(string asset) public void AddJavascriptAsset(string asset)
=> AdditionalJavascriptAssets.Add(asset); => AdditionalJavascriptAssets.Add(asset);
public string[] GetCssAssets()
{
if (HasBeenCollected)
return CssFiles;
CollectAssets();
return CssFiles;
}
public string[] GetJavascriptAssets() public string[] GetJavascriptAssets()
{ {
if (HasBeenCollected) 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; namespace Moonlight.ApiServer.Services;
using Microsoft.Extensions.Hosting.Internal;
using MoonCore.Helpers;
namespace Moonlight.ApiServer.Services; public class BundleService
public class BundleService : IHostedService
{ {
private readonly ILogger<BundleService> Logger;
private readonly List<string> CssFiles = new(); private readonly List<string> CssFiles = new();
private readonly AssetService AssetService; public void BundleCss(string path)
private readonly IWebHostEnvironment HostEnvironment; => CssFiles.Add(path);
public BundleService(ILogger<BundleService> logger, PluginService pluginService, IWebHostEnvironment hostEnvironment, AssetService assetService) public IEnumerable<string> GetCssFiles() => CssFiles;
{
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;
} }

View File

@@ -51,6 +51,9 @@ public class Startup
// Plugin Loading // Plugin Loading
private PluginService PluginService; private PluginService PluginService;
private PluginLoaderService PluginLoaderService; private PluginLoaderService PluginLoaderService;
// Asset bundling
private BundleService BundleService;
private IAppStartup[] PluginAppStartups; private IAppStartup[] PluginAppStartups;
private IDatabaseStartup[] PluginDatabaseStartups; private IDatabaseStartup[] PluginDatabaseStartups;
@@ -150,7 +153,7 @@ public class Startup
var assetService = WebApplication.Services.GetRequiredService<AssetService>(); 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]; var currentArg = Args[i];
@@ -177,7 +180,7 @@ public class Startup
switch (extension) switch (extension)
{ {
case ".css": case ".css":
assetService.AddCssAsset(nextArg); BundleService.BundleCss(nextArg);
break; break;
case ".js": case ".js":
assetService.AddJavascriptAsset(nextArg); assetService.AddJavascriptAsset(nextArg);
@@ -316,7 +319,10 @@ public class Startup
// Configure base services for initialisation // Configure base services for initialisation
initialisationServiceCollection.AddSingleton(Configuration); initialisationServiceCollection.AddSingleton(Configuration);
BundleService = new BundleService();
initialisationServiceCollection.AddSingleton(BundleService);
initialisationServiceCollection.AddLogging(builder => { builder.AddProviders(LoggerProviders); }); initialisationServiceCollection.AddLogging(builder => { builder.AddProviders(LoggerProviders); });
// Configure plugin loading by using the interface service // Configure plugin loading by using the interface service
@@ -344,8 +350,9 @@ public class Startup
private Task RegisterPluginAssets() private Task RegisterPluginAssets()
{ {
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<BundleService>()); WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<BundleGenerationService>());
WebApplicationBuilder.Services.AddSingleton<BundleService>(); WebApplicationBuilder.Services.AddSingleton<BundleGenerationService>();
WebApplicationBuilder.Services.AddSingleton(BundleService);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -134,9 +134,6 @@ public class Startup
var assetManifest = await apiClient.GetJson<FrontendAssetResponse>("api/assets"); var assetManifest = await apiClient.GetJson<FrontendAssetResponse>("api/assets");
var jsRuntime = WebAssemblyHost.Services.GetRequiredService<IJSRuntime>(); 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) foreach (var javascriptFile in assetManifest.JavascriptFiles)
await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", javascriptFile); await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", javascriptFile);

View File

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

View File

@@ -27,15 +27,6 @@ window.moonlight = {
} }
}, },
assets: { 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) { loadJavascript: function (url) {
let scriptElement = document.createElement('script'); let scriptElement = document.createElement('script');

View File

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