Added css bundle api. Improved css bundling code
I made the code cleaner as requested @Masu-Baumgartner :>
This commit is contained in:
@@ -20,7 +20,6 @@ public class AssetsController : Controller
|
|||||||
{
|
{
|
||||||
return new FrontendAssetResponse()
|
return new FrontendAssetResponse()
|
||||||
{
|
{
|
||||||
CssFiles = AssetService.GetCssAssets(),
|
|
||||||
JavascriptFiles = AssetService.GetJavascriptAssets(),
|
JavascriptFiles = AssetService.GetJavascriptAssets(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
|
||||||
@@ -39,22 +30,9 @@ 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)
|
||||||
|
|||||||
140
Moonlight.ApiServer/Services/BundleGenerationService.cs
Normal file
140
Moonlight.ApiServer/Services/BundleGenerationService.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,9 @@ public class Startup
|
|||||||
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;
|
||||||
private IEndpointStartup[] PluginEndpointStartups;
|
private IEndpointStartup[] PluginEndpointStartups;
|
||||||
@@ -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);
|
||||||
@@ -317,6 +320,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,9 +135,6 @@ public class Startup
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user