Skip to content

Commit

Permalink
Enable WasmFingerprintAssets for cache busting (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
ScarletKuro authored Nov 22, 2024
1 parent 34f7d44 commit 6bc4ba2
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 89 deletions.
89 changes: 30 additions & 59 deletions src/Try.Core/CompilationService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 = @"
<MudDialogProvider FullWidth=""true"" MaxWidth=""MaxWidth.ExtraSmall"" />
Expand All @@ -53,9 +54,8 @@ @using MudBlazor
ConfigurationName: "Blazor",
Extensions: ImmutableArray<RazorExtension>.Empty);

public static async Task InitAsync(HttpClient httpClient)
public static async Task InitAsync(Func<IReadOnlyCollection<string>, ValueTask<IReadOnlyList<byte[]>>> getReferencedDllsBytesFunc)
{

var basicReferenceAssemblyRoots = new[]
{
typeof(Console).Assembly, // System.Console
Expand All @@ -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(
Expand All @@ -112,10 +105,7 @@ public async Task<CompileToAssemblyResult> CompileToAssemblyAsync(
ICollection<CodeFile> codeFiles,
Func<string, Task> updateStatusFunc) // TODO: try convert to event
{
if (codeFiles == null)
{
throw new ArgumentNullException(nameof(codeFiles));
}
ArgumentNullException.ThrowIfNull(codeFiles);

var cSharpResults = await this.CompileToCSharpAsync(codeFiles, updateStatusFunc);

Expand All @@ -125,25 +115,6 @@ public async Task<CompileToAssemblyResult> CompileToAssemblyAsync(
return result;
}

private static async Task<IDictionary<string, Stream>> GetStreamFromHttpAsync(
HttpClient httpClient,
IEnumerable<string> assemblyNames)
{
var streams = new ConcurrentDictionary<string, Stream>();

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<CompileToCSharpResult> cSharpResults)
{
if (cSharpResults.Any(r => r.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)))
Expand Down Expand Up @@ -288,16 +259,16 @@ private async Task<IReadOnlyList<CompileToCSharpResult>> CompileToCSharpAsync(
}

private RazorProjectEngine CreateRazorProjectEngine(IReadOnlyList<MetadataReference> 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 });
});
}
}
25 changes: 13 additions & 12 deletions src/TryMudBlazor.Client/Models/TryConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
}
2 changes: 1 addition & 1 deletion src/TryMudBlazor.Client/Pages/Repl.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/TryMudBlazor.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void Log<TState>(
{
if (exception?.ToString()?.Contains(CompilationService.DefaultRootNamespace) ?? false)
{
_jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDLL, CoreConstants.DefaultUserComponentsAssemblyBytes);
_jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, CoreConstants.DefaultUserComponentsAssemblyBytes);
}
}

Expand Down
26 changes: 14 additions & 12 deletions src/TryMudBlazor.Client/Shared/MainLayout.razor.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
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);

if (firstRender)
{
await ApplyUserPreferences();
await CompilationService.InitAsync(GetReferenceAssembliesStreamsAsync);
StateHasChanged();
}
}

private ValueTask<IReadOnlyList<byte[]>> GetReferenceAssembliesStreamsAsync(IReadOnlyCollection<string> referenceAssemblyNames)
{
return JsRuntime.InvokeAsync<IReadOnlyList<byte[]>>(CodeExecution.GetCompilationDlls, new List<string>(referenceAssemblyNames) { "netstandard" });
}

private async Task ApplyUserPreferences()
{
var defaultDarkMode = await _mudThemeProvider.GetSystemPreference();
Expand Down
1 change: 0 additions & 1 deletion src/TryMudBlazor.Client/TryMudBlazor.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<WasmEnableWebcil>false</WasmEnableWebcil>
<WasmFingerprintAssets>false</WasmFingerprintAssets>
</PropertyGroup>

<ItemGroup>
Expand Down
21 changes: 19 additions & 2 deletions src/TryMudBlazor.Client/wwwroot/editor/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down

0 comments on commit 6bc4ba2

Please sign in to comment.