-
Notifications
You must be signed in to change notification settings - Fork 49
Faster preview on no change, future hot reload #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
25fb109
5cc9d07
de12802
85f2ba4
c28dfe0
8001828
63d3092
3a99aca
cbc7695
a765d18
7878033
a6c5e6d
b81658b
82621bd
5f5e889
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,34 @@ | ||
| @using System.Reflection | ||
| @using TryMudBlazor.Client.Pages | ||
| @using TryMudBlazor.Client.Services | ||
| @implements IDisposable | ||
|
|
||
| <Router AppAssembly="@typeof(Program).Assembly" | ||
| AdditionalAssemblies="@(new List<System.Reflection.Assembly> { typeof(Try.UserComponents.__Main).Assembly })"> | ||
| AdditionalAssemblies="@_additionalAssemblies" | ||
| NotFoundPage="typeof(NotFound)"> | ||
| <Found Context="routeData"> | ||
| <RouteView RouteData="@routeData" DefaultLayout="@typeof(EmptyLayout)" /> | ||
| </Found> | ||
| <NotFound> | ||
| <LayoutView Layout="@typeof(EmptyLayout)"> | ||
| <p>Sorry, there's nothing at this address.</p> | ||
| </LayoutView> | ||
| </NotFound> | ||
| </Router> | ||
|
|
||
| @code { | ||
| private readonly Assembly[] _additionalAssemblies; | ||
| private readonly IDisposable _subscription; | ||
|
|
||
| public App(UserComponentsAssemblyService assemblyService) | ||
| { | ||
| _additionalAssemblies = [assemblyService.Assembly]; | ||
| _subscription = assemblyService.Subscribe(HandleAssemblyChanged); | ||
| } | ||
|
|
||
| private Task HandleAssemblyChanged(Assembly assembly) | ||
| { | ||
| _additionalAssemblies[0] = assembly; | ||
| return InvokeAsync(StateHasChanged); | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| _subscription.Dispose(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| using System.Reflection; | ||
| using Microsoft.AspNetCore.Components.WebAssembly.Hosting; | ||
| using Try.UserComponents; | ||
|
|
||
| namespace TryMudBlazor.Client.Extensions; | ||
|
|
||
| internal static class WebAssemblyHostBuilderExtensions | ||
| { | ||
| private static readonly MethodInfo ConfigureMethod = ResolveConfigureMethod(); | ||
|
|
||
| public static void TryInvokeUserStartup(this WebAssemblyHostBuilder builder) | ||
| => ConfigureMethod?.Invoke(null, [builder]); | ||
|
Comment on lines
+9
to
+12
|
||
|
|
||
| private static MethodInfo ResolveConfigureMethod() | ||
| { | ||
| var assembly = typeof(__Main).Assembly; | ||
|
|
||
| var startupType = | ||
| assembly.GetType("UserStartup", throwOnError: false, ignoreCase: true) ?? | ||
| assembly.GetType("Try.UserComponents.UserStartup", throwOnError: false, ignoreCase: true); | ||
|
|
||
| var method = startupType?.GetMethod("Configure", BindingFlags.Static | BindingFlags.Public); | ||
| if (method is null) | ||
| return null; | ||
|
|
||
| var parameters = method.GetParameters(); | ||
| if (parameters.Length != 1 || parameters[0].ParameterType != typeof(WebAssemblyHostBuilder)) | ||
| return null; | ||
|
|
||
| return method; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| @page "/not-found" | ||
| @layout EmptyLayout | ||
|
|
||
| <p>Sorry, there's nothing at this address.</p> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||||||||||||||
| @page "/__main" | ||||||||||||||||||
| @using Microsoft.JSInterop | ||||||||||||||||||
| @using System.Runtime.Loader | ||||||||||||||||||
| @using Try.UserComponents | ||||||||||||||||||
| @using TryMudBlazor.Client.Services | ||||||||||||||||||
| @inject IJSRuntime JSRuntime | ||||||||||||||||||
| @inject UserComponentsAssemblyService AssemblyService | ||||||||||||||||||
| @implements IAsyncDisposable | ||||||||||||||||||
|
|
||||||||||||||||||
| <MudDialogProvider FullWidth="true" MaxWidth="MaxWidth.ExtraSmall" /> | ||||||||||||||||||
| <MudSnackbarProvider /> | ||||||||||||||||||
|
|
||||||||||||||||||
| <DynamicComponent Type="@_type" /> | ||||||||||||||||||
|
|
||||||||||||||||||
| @code { | ||||||||||||||||||
| private Type _type = typeof(__Main); | ||||||||||||||||||
| private AssemblyLoadContext _loadContext; | ||||||||||||||||||
| private readonly Lazy<DotNetObjectReference<UserMainPage>> _ref; | ||||||||||||||||||
|
|
||||||||||||||||||
| public UserMainPage() | ||||||||||||||||||
| { | ||||||||||||||||||
| _ref = new Lazy<DotNetObjectReference<UserMainPage>>(CreateDotNetObject()); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| protected override async Task OnAfterRenderAsync(bool firstRender) | ||||||||||||||||||
| { | ||||||||||||||||||
| if (firstRender) | ||||||||||||||||||
| { | ||||||||||||||||||
| await JSRuntime.InvokeVoidAsync("Try.registerHotReload", _ref.Value); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| [JSInvokable] | ||||||||||||||||||
| public async Task HotReload() | ||||||||||||||||||
| { | ||||||||||||||||||
| try | ||||||||||||||||||
| { | ||||||||||||||||||
| var base64 = await JSRuntime.InvokeAsync<string>("sessionStorage.getItem", "TryMudBlazor.UserComponentsDllBase64"); | ||||||||||||||||||
| if (base64 == null) | ||||||||||||||||||
| { | ||||||||||||||||||
| // sessionStorage was cleared (e.g. crash recovery) — fall back to a full reload | ||||||||||||||||||
| // so loadBootResource picks up the safe static DLL. | ||||||||||||||||||
| await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| var bytes = Convert.FromBase64String(base64); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Swap to a fresh collectible context; previous one becomes eligible for GC | ||||||||||||||||||
| // once Blazor disposes the old component instance below. | ||||||||||||||||||
| var previousContext = _loadContext; | ||||||||||||||||||
| _loadContext = new AssemblyLoadContext(name: null, isCollectible: true); | ||||||||||||||||||
| var assembly = _loadContext.LoadFromStream(new MemoryStream(bytes)); | ||||||||||||||||||
| previousContext?.Unload(); | ||||||||||||||||||
|
Comment on lines
+51
to
+54
|
||||||||||||||||||
|
|
||||||||||||||||||
| // UserStartup requires DI re-registration — fall back to full iframe reload | ||||||||||||||||||
| if (assembly.GetType("UserStartup") is not null || | ||||||||||||||||||
| assembly.GetType("Try.UserComponents.UserStartup") is not null) | ||||||||||||||||||
| { | ||||||||||||||||||
| await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| var newType = assembly.GetType("Try.UserComponents.__Main"); | ||||||||||||||||||
| if (newType is not null) | ||||||||||||||||||
| { | ||||||||||||||||||
| // Update the Router's AdditionalAssemblies before rendering the new __Main | ||||||||||||||||||
| // so that any NavigateTo calls inside it can resolve user-defined @page routes. | ||||||||||||||||||
| await AssemblyService.UpdateAssemblyAsync(assembly); | ||||||||||||||||||
| _type = newType; | ||||||||||||||||||
| await InvokeAsync(StateHasChanged); | ||||||||||||||||||
| } | ||||||||||||||||||
|
||||||||||||||||||
| } | |
| } | |
| else | |
| { | |
| // No __Main component found in the updated assembly — fall back to a full reload | |
| // to avoid leaving the UI in an inconsistent state. | |
| await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is impossible scenario
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The catch block at line 74 swallows all exceptions without logging them. This makes debugging difficult when hot reload fails for unexpected reasons. Consider logging the exception before falling back to a full reload, especially during development. At minimum, log to the browser console using JSRuntime or Console.WriteLine.
| catch | |
| { | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Hot reload failed, falling back to full reload: {ex}"); |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If HotReload is called multiple times in quick succession (e.g., rapid user edits), there's a potential for race conditions. The _loadContext could be unloaded while a previous HotReload call is still processing, or _type could be updated mid-render. Consider adding a semaphore or checking if a reload is already in progress, and either queuing subsequent requests or ignoring them until the current reload completes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not called on users edits, for now. I prefer yagni here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The NotFoundPage property expects a Type, but it's being set to a string expression "typeof(NotFound)". This should be @typeof(NotFound) or simply typeof(NotFound) without quotes. As written, this will cause a compilation error since the Router component expects Type, not string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No comments here...