From 6bc4ba2096c1a24606a69605b2d0f6c79a3a5024 Mon Sep 17 00:00:00 2001 From: "Artyom M." Date: Sat, 23 Nov 2024 00:51:43 +0200 Subject: [PATCH] Enable WasmFingerprintAssets for cache busting (#166) --- src/Try.Core/CompilationService.cs | 89 +++++++------------ .../Models/TryConstants.cs | 25 +++--- src/TryMudBlazor.Client/Pages/Repl.razor.cs | 2 +- src/TryMudBlazor.Client/Program.cs | 2 +- ...leCriticalUserComponentExceptionsLogger.cs | 2 +- .../Shared/MainLayout.razor.cs | 26 +++--- .../TryMudBlazor.Client.csproj | 1 - .../wwwroot/editor/main.js | 21 ++++- 8 files changed, 79 insertions(+), 89 deletions(-) diff --git a/src/Try.Core/CompilationService.cs b/src/Try.Core/CompilationService.cs index 816ab2de..87b7070e 100644 --- a/src/Try.Core/CompilationService.cs +++ b/src/Try.Core/CompilationService.cs @@ -1,7 +1,6 @@ namespace Try.Core { using System; - using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; @@ -25,16 +24,18 @@ public class CompilationService public const string DefaultRootNamespace = $"{nameof(Try)}.{nameof(UserComponents)}"; private const string WorkingDirectory = "/TryMudBlazor/"; - private const string DefaultImports = @"@using System.ComponentModel.DataAnnotations -@using System.Linq -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.JSInterop -@using MudBlazor -"; + private static readonly string[] DefaultImports = + [ + "@using System.ComponentModel.DataAnnotations", + "@using System.Linq", + "@using System.Net.Http", + "@using System.Net.Http.Json", + "@using Microsoft.AspNetCore.Components.Forms", + "@using Microsoft.AspNetCore.Components.Routing", + "@using Microsoft.AspNetCore.Components.Web", + "@using Microsoft.JSInterop", + "@using MudBlazor" + ]; private const string MudBlazorServices = @" @@ -53,9 +54,8 @@ @using MudBlazor ConfigurationName: "Blazor", Extensions: ImmutableArray.Empty); - public static async Task InitAsync(HttpClient httpClient) + public static async Task InitAsync(Func, ValueTask>> getReferencedDllsBytesFunc) { - var basicReferenceAssemblyRoots = new[] { typeof(Console).Assembly, // System.Console @@ -71,23 +71,16 @@ public static async Task InitAsync(HttpClient httpClient) typeof(WebAssemblyHostBuilder).Assembly, // Microsoft.AspNetCore.Components.WebAssembly typeof(FluentValidation.AbstractValidator<>).Assembly, }; - - var assemblyNames = basicReferenceAssemblyRoots - .SelectMany(assembly => assembly.GetReferencedAssemblies().Concat(new[] { assembly.GetName() })) - .Select(x => x.Name) - .Distinct() - .ToList(); - - var assemblyStreams = await GetStreamFromHttpAsync(httpClient, assemblyNames); - - var allReferenceAssemblies = assemblyStreams.ToDictionary(a => a.Key, a => MetadataReference.CreateFromStream(a.Value)); - - var basicReferenceAssemblies = allReferenceAssemblies - .Where(a => basicReferenceAssemblyRoots - .Select(x => x.GetName().Name) - .Union(basicReferenceAssemblyRoots.SelectMany(y => y.GetReferencedAssemblies().Select(z => z.Name))) - .Any(n => n == a.Key)) - .Select(a => a.Value) + var assemblyNames = await getReferencedDllsBytesFunc(basicReferenceAssemblyRoots + .SelectMany(assembly => assembly.GetReferencedAssemblies().Concat( + [ + assembly.GetName() + ])) + .Select(assemblyName => assemblyName.Name) + .ToHashSet()); + + var basicReferenceAssemblies = assemblyNames + .Select(peImage => MetadataReference.CreateFromImage(peImage, MetadataReferenceProperties.Assembly)) .ToList(); _baseCompilation = CSharpCompilation.Create( @@ -112,10 +105,7 @@ public async Task CompileToAssemblyAsync( ICollection codeFiles, Func updateStatusFunc) // TODO: try convert to event { - if (codeFiles == null) - { - throw new ArgumentNullException(nameof(codeFiles)); - } + ArgumentNullException.ThrowIfNull(codeFiles); var cSharpResults = await this.CompileToCSharpAsync(codeFiles, updateStatusFunc); @@ -125,25 +115,6 @@ public async Task CompileToAssemblyAsync( return result; } - private static async Task> GetStreamFromHttpAsync( - HttpClient httpClient, - IEnumerable assemblyNames) - { - var streams = new ConcurrentDictionary(); - - await Task.WhenAll( - assemblyNames.Select(async assemblyName => - { - var result = await httpClient.GetAsync($"/_framework/{assemblyName}.dll"); - - result.EnsureSuccessStatusCode(); - - streams.TryAdd(assemblyName, await result.Content.ReadAsStreamAsync()); - })); - - return streams; - } - private static CompileToAssemblyResult CompileToAssembly(IReadOnlyList cSharpResults) { if (cSharpResults.Any(r => r.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))) @@ -288,16 +259,16 @@ private async Task> CompileToCSharpAsync( } private RazorProjectEngine CreateRazorProjectEngine(IReadOnlyList references) => - RazorProjectEngine.Create(configuration, fileSystem, b => + RazorProjectEngine.Create(configuration, fileSystem, builder => { - b.SetRootNamespace(DefaultRootNamespace); - b.AddDefaultImports(DefaultImports); + builder.SetRootNamespace(DefaultRootNamespace); + builder.AddDefaultImports(DefaultImports); // Features that use Roslyn are mandatory for components - CompilerFeatures.Register(b); + CompilerFeatures.Register(builder); - b.Features.Add(new CompilationTagHelperFeature()); - b.Features.Add(new DefaultMetadataReferenceFeature { References = references }); + builder.Features.Add(new CompilationTagHelperFeature()); + builder.Features.Add(new DefaultMetadataReferenceFeature { References = references }); }); } } diff --git a/src/TryMudBlazor.Client/Models/TryConstants.cs b/src/TryMudBlazor.Client/Models/TryConstants.cs index cc200319..07593ea4 100644 --- a/src/TryMudBlazor.Client/Models/TryConstants.cs +++ b/src/TryMudBlazor.Client/Models/TryConstants.cs @@ -2,24 +2,25 @@ { public static class Try { - public static string Initialize = "Try.initialize"; - public static string ChangeDisplayUrl = "Try.changeDisplayUrl"; - public static string ReloadIframe = "Try.reloadIframe"; - public static string Dispose = "Try.dispose"; + public const string Initialize = "Try.initialize"; + public const string ChangeDisplayUrl = "Try.changeDisplayUrl"; + public const string ReloadIframe = "Try.reloadIframe"; + public const string Dispose = "Try.dispose"; public static class Editor { - public static string Create = "Try.Editor.create"; - public static string GetValue = "Try.Editor.getValue"; - public static string SetValue = "Try.Editor.setValue"; - public static string SetLangugage = "Try.Editor.setLanguage"; - public static string Focus = "Try.Editor.focus"; - public static string SetTheme = "Try.Editor.setTheme"; - public static string Dispose = "Try.Editor.dispose"; + public const string Create = "Try.Editor.create"; + public const string GetValue = "Try.Editor.getValue"; + public const string SetValue = "Try.Editor.setValue"; + public const string SetLangugage = "Try.Editor.setLanguage"; + public const string Focus = "Try.Editor.focus"; + public const string SetTheme = "Try.Editor.setTheme"; + public const string Dispose = "Try.Editor.dispose"; } public static class CodeExecution { - public static string UpdateUserComponentsDLL = "Try.CodeExecution.updateUserComponentsDll"; + public const string GetCompilationDlls = "Try.CodeExecution.getCompilationDlls"; + public const string UpdateUserComponentsDll = "Try.CodeExecution.updateUserComponentsDll"; } } } diff --git a/src/TryMudBlazor.Client/Pages/Repl.razor.cs b/src/TryMudBlazor.Client/Pages/Repl.razor.cs index 51a6dd93..984afa2f 100644 --- a/src/TryMudBlazor.Client/Pages/Repl.razor.cs +++ b/src/TryMudBlazor.Client/Pages/Repl.razor.cs @@ -201,7 +201,7 @@ private async Task CompileAsync() if (compilationResult?.AssemblyBytes?.Length > 0) { // Make sure the DLL is updated before reloading the user page - await this.JsRuntime.InvokeVoidAsync(Try.CodeExecution.UpdateUserComponentsDLL, compilationResult.AssemblyBytes); + await this.JsRuntime.InvokeVoidAsync(Try.CodeExecution.UpdateUserComponentsDll, compilationResult.AssemblyBytes); // TODO: Add error page in iframe this.JsRuntime.InvokeVoid(Try.ReloadIframe, "user-page-window", MainUserPagePath); diff --git a/src/TryMudBlazor.Client/Program.cs b/src/TryMudBlazor.Client/Program.cs index c4899c77..ff92e761 100644 --- a/src/TryMudBlazor.Client/Program.cs +++ b/src/TryMudBlazor.Client/Program.cs @@ -55,7 +55,7 @@ public static async Task Main(string[] args) var actualException = exception is TargetInvocationException tie ? tie.InnerException : exception; await Console.Error.WriteLineAsync($"Error on app startup: {actualException}"); - jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDLL, CoreConstants.DefaultUserComponentsAssemblyBytes); + jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, CoreConstants.DefaultUserComponentsAssemblyBytes); } await builder.Build().RunAsync(); diff --git a/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs b/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs index e6a8f488..c0a761cb 100644 --- a/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs +++ b/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs @@ -28,7 +28,7 @@ public void Log( { if (exception?.ToString()?.Contains(CompilationService.DefaultRootNamespace) ?? false) { - _jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDLL, CoreConstants.DefaultUserComponentsAssemblyBytes); + _jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, CoreConstants.DefaultUserComponentsAssemblyBytes); } } diff --git a/src/TryMudBlazor.Client/Shared/MainLayout.razor.cs b/src/TryMudBlazor.Client/Shared/MainLayout.razor.cs index e43335a0..b94031ff 100644 --- a/src/TryMudBlazor.Client/Shared/MainLayout.razor.cs +++ b/src/TryMudBlazor.Client/Shared/MainLayout.razor.cs @@ -1,34 +1,30 @@ namespace TryMudBlazor.Client.Shared { using System; - using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; + using Microsoft.JSInterop; using MudBlazor; using Services; using Try.Core; - using TryMudBlazor.Client; + using static TryMudBlazor.Client.Models.Try; public partial class MainLayout : LayoutComponentBase, IDisposable { - [Inject] public HttpClient HttpClient { get; set; } - [Inject] private LayoutService LayoutService { get; set; } - private MudThemeProvider _mudThemeProvider; + [Inject] + private LayoutService LayoutService { get; set; } + + [Inject] + private IJSRuntime JsRuntime { get; set; } + protected override void OnInitialized() { LayoutService.MajorUpdateOccured += LayoutServiceOnMajorUpdateOccured; base.OnInitialized(); } - protected override async Task OnInitializedAsync() - { - await CompilationService.InitAsync(this.HttpClient); - - await base.OnInitializedAsync(); - } - protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); @@ -36,10 +32,16 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { await ApplyUserPreferences(); + await CompilationService.InitAsync(GetReferenceAssembliesStreamsAsync); StateHasChanged(); } } + private ValueTask> GetReferenceAssembliesStreamsAsync(IReadOnlyCollection referenceAssemblyNames) + { + return JsRuntime.InvokeAsync>(CodeExecution.GetCompilationDlls, new List(referenceAssemblyNames) { "netstandard" }); + } + private async Task ApplyUserPreferences() { var defaultDarkMode = await _mudThemeProvider.GetSystemPreference(); diff --git a/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj b/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj index 1af9f190..aea56528 100644 --- a/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj +++ b/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj @@ -3,7 +3,6 @@ false false - false diff --git a/src/TryMudBlazor.Client/wwwroot/editor/main.js b/src/TryMudBlazor.Client/wwwroot/editor/main.js index 227150d1..f4993270 100644 --- a/src/TryMudBlazor.Client/wwwroot/editor/main.js +++ b/src/TryMudBlazor.Client/wwwroot/editor/main.js @@ -205,6 +205,22 @@ window.Try.CodeExecution = window.Try.CodeExecution || (function () { } return { + getCompilationDlls: async function (dllNames) { + const cache = await caches.open('dotnet-resources-/'); + const keys = await cache.keys(); + const dllsData = []; + await Promise.all(dllNames.map(async (dll) => { + // Requires WasmFingerprintAssets to be enabled + const pattern = new RegExp(`${dll}.[^\\.]*\\.dll`, 'i'); + const dllKey = keys.find(x => pattern.test(x.url)).url.substring(window.location.origin.length); + const response = await cache.match(dllKey); + const bytes = new Uint8Array(await response.arrayBuffer()); + dllsData.push(bytes); + })); + + return dllsData; + }, + updateUserComponentsDll: async function (fileContent) { if (!fileContent) { return; @@ -213,13 +229,14 @@ window.Try.CodeExecution = window.Try.CodeExecution || (function () { const cache = await caches.open('dotnet-resources-/'); const cacheKeys = await cache.keys(); - const userComponentsDllCacheKey = cacheKeys.find(x => /Try\.UserComponents\.dll/.test(x.url)); + // Requires WasmFingerprintAssets to be enabled + const userComponentsDllCacheKey = cacheKeys.find(x => /Try\.UserComponents\.[^/]*\.dll/.test(x.url)); if (!userComponentsDllCacheKey || !userComponentsDllCacheKey.url) { alert(UNEXPECTED_ERROR_MESSAGE); return; } - const dllPath = userComponentsDllCacheKey.url.substr(window.location.origin.length); + const dllPath = userComponentsDllCacheKey.url.substring(window.location.origin.length); fileContent = typeof fileContent === 'number' ? BINDING.conv_string(fileContent) : fileContent // tranfering raw pointer to the memory of the mono string const dllBytes = typeof fileContent === 'string' ? convertBase64StringToBytes(fileContent) : fileContent;