Skip to content

Commit 8372b35

Browse files
authored
Faster preview on no change, future hot reload (#192)
1 parent 046f7da commit 8372b35

19 files changed

Lines changed: 255 additions & 69 deletions

src/Try.Core/CompilationService.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,6 @@ public class CompilationService
3838
"@using MudBlazor"
3939
];
4040

41-
private const string MudBlazorServices = @"
42-
<MudDialogProvider FullWidth=""true"" MaxWidth=""MaxWidth.ExtraSmall"" />
43-
<MudSnackbarProvider/>
44-
45-
";
46-
4741
// Creating the initial compilation + reading references is on the order of 250ms without caching
4842
// so making sure it doesn't happen for each run.
4943
private static CSharpCompilation _baseCompilation;
@@ -71,6 +65,7 @@ public static unsafe Task InitAsync()
7165
typeof(AssemblyTargetedPatchBandAttribute).Assembly, // System.Private.CoreLib
7266
typeof(NavLink).Assembly, // Microsoft.AspNetCore.Components.Web
7367
typeof(IQueryable).Assembly, // System.Linq.Expressions
68+
typeof(Queryable).Assembly, // System.Linq.Queryable
7469
typeof(HttpClientJsonExtensions).Assembly, // System.Net.Http.Json
7570
typeof(HttpClient).Assembly, // System.Net.Http
7671
typeof(IJSRuntime).Assembly, // Microsoft.JSInterop
@@ -129,6 +124,7 @@ public async Task<CompileToAssemblyResult> CompileToAssemblyAsync(
129124
var codeHash = ComputeCodeHash(codeFiles);
130125
if (_cachedResult != null && _lastCodeHash == codeHash)
131126
{
127+
_cachedResult.IsFromCache = true;
132128
return _cachedResult;
133129
}
134130

@@ -258,9 +254,7 @@ private async Task<IReadOnlyList<CompileToCSharpResult>> CompileToCSharpAsync(
258254
{
259255
if (codeFile.Type == CodeFileType.Razor)
260256
{
261-
var fileContent = index == 0 ? MudBlazorServices : string.Empty;
262-
fileContent += codeFile.Content;
263-
var projectItem = CreateRazorProjectItem(codeFile.Path, fileContent);
257+
var projectItem = CreateRazorProjectItem(codeFile.Path, codeFile.Content);
264258

265259
var codeDocument = projectEngine.ProcessDeclarationOnly(projectItem);
266260
var cSharpDocument = codeDocument.GetCSharpDocument();

src/Try.Core/CompileToAssemblyResult.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ public class CompileToAssemblyResult
1010
public IEnumerable<CompilationDiagnostic> Diagnostics { get; set; } = [];
1111

1212
public byte[] AssemblyBytes { get; set; }
13+
14+
public bool IsFromCache { get; set; }
1315
}
1416
}

src/Try.Core/CoreConstants.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ void ButtonOnClick()
2020
}
2121
}";
2222

23-
public const string DefaultUserComponentsAssemblyBytes =
24-
"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAABQRQAATAEDAF0sHZkAAAAAAAAAAOAAIiALATAAAAgAAAAGAAAAAAAA9iYAAAAgAAAAQAAAAAAAEAAgAAAAAgAABAAAAAAAAAAEAAAAAAAAAACAAAAAAgAAAAAAAAMAYIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAKImAABPAAAAAEAAANQDAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAwAAACoJQAAVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAACAAAAAAAAAAAAAAACCAAAEgAAAAAAAAAAAAAAC50ZXh0AAAA/AYAAAAgAAAACAAAAAIAAAAAAAAAAAAAAAAAACAAAGAucnNyYwAAANQDAAAAQAAAAAQAAAAKAAAAAAAAAAAAAAAAAABAAABALnJlbG9jAAAMAAAAAGAAAAACAAAADgAAAAAAAAAAAAAAAAAAQAAAQgAAAAAAAAAAAAAAAAAAAADWJgAAAAAAAEgAAAACAAUAXCAAAEwFAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYqHgIoDAAACioAAEJTSkIBAAEAAAAAAAwAAAB2NC4wLjMwMzE5AAAAAAUAbAAAAKQBAAAjfgAAEAIAAEwCAAAjU3RyaW5ncwAAAABcBAAABAAAACNVUwBgBAAAEAAAACNHVUlEAAAAcAQAANwAAAAjQmxvYgAAAAAAAAACAAABRxUAAAkAAAAA+gEzABYAAAEAAAAOAAAAAgAAAAIAAAABAAAADAAAAAsAAAABAAAAAgAAAAAAhQEBAAAAAAAGAOsA6gEGAD0B6gEGADcA1wEPAAoCAAAGACQBowEGALQAowEGAHEAowEGAI4AowEGAAsBowEGAEsAowEGANMA6gEKAGIAGQIKACkAGQIKALUBWwEAAAAAAQAAAAAAAQABAAEAEACcATkCNQABAAEAUCAAAAAAxAAKACcAAQBSIAAAAACGGNEBBgACAAAAAQDHAQkA0QEBABEA0QEGABkA0QEKACkA0QEQADEA0QEQADkA0QEQAEEA0QEQAEkA0QEQAFEA0QEQAFkA0QEBAGEA0QEQAGkA0QEGACcAUwDRAC4ACwAtAC4AEwA2AC4AGwBVAC4AIwBeAC4AKwB2AC4AMwCDAC4AOwCQAC4AQwBeAC4ASwBeAEMAWwDEAASAAAABAAAAAAAAAAAAAAAAADkCAAAJAAAAAAAAAAAAAAAVABoAAAAAAAkAAAAAAAAAAAAAAB4AGQIAAAAAAAAAAAA8TW9kdWxlPgBCdWlsZFJlbmRlclRyZWUAU3lzdGVtLlJ1bnRpbWUAQ29tcG9uZW50QmFzZQBEZWJ1Z2dhYmxlQXR0cmlidXRlAEFzc2VtYmx5VGl0bGVBdHRyaWJ1dGUAUm91dGVBdHRyaWJ1dGUAQXNzZW1ibHlGaWxlVmVyc2lvbkF0dHJpYnV0ZQBBc3NlbWJseUluZm9ybWF0aW9uYWxWZXJzaW9uQXR0cmlidXRlAEFzc2VtYmx5Q29uZmlndXJhdGlvbkF0dHJpYnV0ZQBSZWZTYWZldHlSdWxlc0F0dHJpYnV0ZQBDb21waWxhdGlvblJlbGF4YXRpb25zQXR0cmlidXRlAEFzc2VtYmx5UHJvZHVjdEF0dHJpYnV0ZQBBc3NlbWJseUNvbXBhbnlBdHRyaWJ1dGUAUnVudGltZUNvbXBhdGliaWxpdHlBdHRyaWJ1dGUATWljcm9zb2Z0LkFzcE5ldENvcmUuQ29tcG9uZW50cy5SZW5kZXJpbmcAVHJ5LlVzZXJDb21wb25lbnRzLmRsbABfX01haW4AU3lzdGVtLlJlZmxlY3Rpb24AUmVuZGVyVHJlZUJ1aWxkZXIAX19idWlsZGVyAC5jdG9yAFN5c3RlbS5EaWFnbm9zdGljcwBTeXN0ZW0uUnVudGltZS5Db21waWxlclNlcnZpY2VzAERlYnVnZ2luZ01vZGVzAE1pY3Jvc29mdC5Bc3BOZXRDb3JlLkNvbXBvbmVudHMAVHJ5LlVzZXJDb21wb25lbnRzAAAAAAAdJaLVPNvETL37jSE7YZGLAAQgAQEIAyAAAQUgAQEREQQgAQEOCLA/X38R1Qo6CK25eTgp3a5gBSABARI5CAEACAAAAAAAHgEAAQBU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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
25-
2623
public const string DefaultRazorFileContentFormat = "<h1>{0}</h1>";
2724

2825
public static readonly string DefaultCSharpFileContentFormat =

src/TryMudBlazor.Client/App.razor

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
1+
@using System.Reflection
2+
@using TryMudBlazor.Client.Pages
3+
@using TryMudBlazor.Client.Services
4+
@implements IDisposable
5+
16
<Router AppAssembly="@typeof(Program).Assembly"
2-
AdditionalAssemblies="@(new List<System.Reflection.Assembly> { typeof(Try.UserComponents.__Main).Assembly })">
7+
AdditionalAssemblies="@_additionalAssemblies"
8+
NotFoundPage="typeof(NotFound)">
39
<Found Context="routeData">
410
<RouteView RouteData="@routeData" DefaultLayout="@typeof(EmptyLayout)" />
511
</Found>
6-
<NotFound>
7-
<LayoutView Layout="@typeof(EmptyLayout)">
8-
<p>Sorry, there's nothing at this address.</p>
9-
</LayoutView>
10-
</NotFound>
1112
</Router>
13+
14+
@code {
15+
private readonly Assembly[] _additionalAssemblies;
16+
private readonly IDisposable _subscription;
17+
18+
public App(UserComponentsAssemblyService assemblyService)
19+
{
20+
_additionalAssemblies = [assemblyService.Assembly];
21+
_subscription = assemblyService.Subscribe(HandleAssemblyChanged);
22+
}
23+
24+
private Task HandleAssemblyChanged(Assembly assembly)
25+
{
26+
_additionalAssemblies[0] = assembly;
27+
return InvokeAsync(StateHasChanged);
28+
}
29+
30+
public void Dispose()
31+
{
32+
_subscription.Dispose();
33+
}
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Reflection;
2+
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
3+
using Try.UserComponents;
4+
5+
namespace TryMudBlazor.Client.Extensions;
6+
7+
internal static class WebAssemblyHostBuilderExtensions
8+
{
9+
private static readonly MethodInfo ConfigureMethod = ResolveConfigureMethod();
10+
11+
public static void TryInvokeUserStartup(this WebAssemblyHostBuilder builder)
12+
=> ConfigureMethod?.Invoke(null, [builder]);
13+
14+
private static MethodInfo ResolveConfigureMethod()
15+
{
16+
var assembly = typeof(__Main).Assembly;
17+
18+
var startupType =
19+
assembly.GetType("UserStartup", throwOnError: false, ignoreCase: true) ??
20+
assembly.GetType("Try.UserComponents.UserStartup", throwOnError: false, ignoreCase: true);
21+
22+
var method = startupType?.GetMethod("Configure", BindingFlags.Static | BindingFlags.Public);
23+
if (method is null)
24+
return null;
25+
26+
var parameters = method.GetParameters();
27+
if (parameters.Length != 1 || parameters[0].ParameterType != typeof(WebAssemblyHostBuilder))
28+
return null;
29+
30+
return method;
31+
}
32+
}

src/TryMudBlazor.Client/Models/TryConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public static class Editor
2020
public static class CodeExecution
2121
{
2222
public const string UpdateUserComponentsDll = "Try.CodeExecution.updateUserComponentsDll";
23+
public const string ClearUserComponentsDll = "Try.CodeExecution.clearUserComponentsDll";
24+
public const string HotReloadIframe = "Try.CodeExecution.hotReloadIframe";
2325
}
2426
}
2527
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@page "/not-found"
2+
@layout EmptyLayout
3+
4+
<p>Sorry, there's nothing at this address.</p>

src/TryMudBlazor.Client/Pages/Repl.razor.cs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public partial class Repl : IDisposable
1818
{
1919
[Inject] private LayoutService LayoutService { get; set; }
2020

21-
private const string MainComponentCodePrefix = "@page \"/__main\"\n";
2221
private const string MainUserPagePath = "/__main";
2322

2423
private DotNetObjectReference<Repl> dotNetInstance;
@@ -166,19 +165,10 @@ private async Task CompileAsync()
166165
await Task.Yield();
167166

168167
CompileToAssemblyResult compilationResult = null;
169-
CodeFile mainComponent = null;
170-
string originalMainComponentContent = null;
171168
try
172169
{
173170
this.UpdateActiveCodeFileContent();
174171

175-
// Add the necessary main component code prefix and store the original content so we can revert right after compilation.
176-
if (this.CodeFiles.TryGetValue(CoreConstants.MainComponentFilePath, out mainComponent))
177-
{
178-
originalMainComponentContent = mainComponent.Content;
179-
mainComponent.Content = MainComponentCodePrefix + originalMainComponentContent.Replace(MainComponentCodePrefix, "");
180-
}
181-
182172
compilationResult = await this.CompilationService.CompileToAssemblyAsync(
183173
this.CodeFiles.Values,
184174
this.UpdateLoaderTextAsync);
@@ -193,21 +183,13 @@ private async Task CompileAsync()
193183
}
194184
finally
195185
{
196-
if (mainComponent != null)
197-
{
198-
mainComponent.Content = originalMainComponentContent;
199-
}
200-
201186
this.Loading = false;
202187
}
203188

204189
if (compilationResult?.AssemblyBytes?.Length > 0)
205190
{
206-
// Make sure the DLL is updated before reloading the user page
207191
this.JsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, compilationResult.AssemblyBytes);
208-
209-
// TODO: Add error page in iframe
210-
this.JsRuntime.InvokeVoid(Try.ReloadIframe, "user-page-window", MainUserPagePath);
192+
this.JsRuntime.InvokeVoid(Try.CodeExecution.HotReloadIframe, "user-page-window", MainUserPagePath);
211193
}
212194
}
213195

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
@page "/__main"
2+
@using Microsoft.JSInterop
3+
@using System.Runtime.Loader
4+
@using Try.UserComponents
5+
@using TryMudBlazor.Client.Services
6+
@inject IJSRuntime JSRuntime
7+
@inject UserComponentsAssemblyService AssemblyService
8+
@implements IAsyncDisposable
9+
10+
<MudDialogProvider FullWidth="true" MaxWidth="MaxWidth.ExtraSmall" />
11+
<MudSnackbarProvider />
12+
13+
<DynamicComponent Type="@_type" />
14+
15+
@code {
16+
private Type _type = typeof(__Main);
17+
private AssemblyLoadContext _loadContext;
18+
private readonly Lazy<DotNetObjectReference<UserMainPage>> _ref;
19+
20+
public UserMainPage()
21+
{
22+
_ref = new Lazy<DotNetObjectReference<UserMainPage>>(CreateDotNetObject());
23+
}
24+
25+
protected override async Task OnAfterRenderAsync(bool firstRender)
26+
{
27+
if (firstRender)
28+
{
29+
await JSRuntime.InvokeVoidAsync("Try.registerHotReload", _ref.Value);
30+
}
31+
}
32+
33+
[JSInvokable]
34+
public async Task HotReload()
35+
{
36+
try
37+
{
38+
var base64 = await JSRuntime.InvokeAsync<string>("sessionStorage.getItem", "TryMudBlazor.UserComponentsDllBase64");
39+
if (base64 == null)
40+
{
41+
// sessionStorage was cleared (e.g. crash recovery) — fall back to a full reload
42+
// so loadBootResource picks up the safe static DLL.
43+
await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main");
44+
return;
45+
}
46+
47+
var bytes = Convert.FromBase64String(base64);
48+
49+
// Swap to a fresh collectible context; previous one becomes eligible for GC
50+
// once Blazor disposes the old component instance below.
51+
var previousContext = _loadContext;
52+
_loadContext = new AssemblyLoadContext(name: null, isCollectible: true);
53+
var assembly = _loadContext.LoadFromStream(new MemoryStream(bytes));
54+
previousContext?.Unload();
55+
56+
// UserStartup requires DI re-registration — fall back to full iframe reload
57+
if (assembly.GetType("UserStartup") is not null ||
58+
assembly.GetType("Try.UserComponents.UserStartup") is not null)
59+
{
60+
await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main");
61+
return;
62+
}
63+
64+
var newType = assembly.GetType("Try.UserComponents.__Main");
65+
if (newType is not null)
66+
{
67+
// Update the Router's AdditionalAssemblies before rendering the new __Main
68+
// so that any NavigateTo calls inside it can resolve user-defined @page routes.
69+
await AssemblyService.UpdateAssemblyAsync(assembly);
70+
_type = newType;
71+
await InvokeAsync(StateHasChanged);
72+
}
73+
}
74+
catch
75+
{
76+
await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main");
77+
}
78+
}
79+
80+
public async ValueTask DisposeAsync()
81+
{
82+
if (_ref.IsValueCreated)
83+
{
84+
await JSRuntime.InvokeVoidAsync("Try.unregisterHotReload");
85+
_ref.Value.Dispose();
86+
}
87+
_loadContext?.Unload();
88+
}
89+
90+
private DotNetObjectReference<UserMainPage> CreateDotNetObject() => DotNetObjectReference.Create(this);
91+
}

src/TryMudBlazor.Client/Program.cs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace TryMudBlazor.Client
1717
using Services.UserPreferences;
1818
using Try.Core;
1919
using Try.UserComponents;
20+
using TryMudBlazor.Client.Extensions;
2021
using TryMudBlazor.Client.Models;
2122
using TryMudBlazor.Client.Services;
2223

@@ -32,6 +33,7 @@ public static async Task Main(string[] args)
3233
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
3334
builder.Services.AddScoped<SnippetsService>();
3435
builder.Services.AddSingleton(new CompilationService());
36+
builder.Services.AddSingleton<UserComponentsAssemblyService>();
3537

3638
builder.Services
3739
.AddOptions<SnippetsOptions>()
@@ -46,7 +48,7 @@ public static async Task Main(string[] args)
4648
var jsRuntime = GetJsRuntime();
4749
try
4850
{
49-
ExecuteUserDefinedConfiguration(builder);
51+
builder.TryInvokeUserStartup();
5052
}
5153
catch (Exception exception)
5254
{
@@ -55,28 +57,12 @@ public static async Task Main(string[] args)
5557
var actualException = exception is TargetInvocationException tie ? tie.InnerException : exception;
5658
await Console.Error.WriteLineAsync($"Error on app startup: {actualException}");
5759

58-
jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, CoreConstants.DefaultUserComponentsAssemblyBytes);
60+
jsRuntime.InvokeVoid(Try.CodeExecution.ClearUserComponentsDll);
5961
}
6062

6163
await builder.Build().RunAsync();
6264
}
6365

64-
private static void ExecuteUserDefinedConfiguration(WebAssemblyHostBuilder builder)
65-
{
66-
var userComponentsAssembly = typeof(__Main).Assembly;
67-
var startupType = userComponentsAssembly.GetType("UserStartup", throwOnError: false, ignoreCase: true)
68-
?? userComponentsAssembly.GetType("Try.UserComponents.UserStartup", throwOnError: false, ignoreCase: true);
69-
if (startupType == null)
70-
return;
71-
var configureMethod = startupType.GetMethod("Configure", BindingFlags.Static | BindingFlags.Public);
72-
if (configureMethod == null)
73-
return;
74-
var configureMethodParams = configureMethod.GetParameters();
75-
if (configureMethodParams.Length != 1 || configureMethodParams[0].ParameterType != typeof(WebAssemblyHostBuilder))
76-
return;
77-
configureMethod.Invoke(obj: null, new object[] { builder });
78-
}
79-
8066
private static IJSInProcessRuntime GetJsRuntime()
8167
{
8268
const string defaultJsRuntimeTypeName = "DefaultWebAssemblyJSRuntime";

0 commit comments

Comments
 (0)