From 25fb1097e0d7a6d54656a65a2fee7ff20915e89b Mon Sep 17 00:00:00 2001 From: ScarletKuro Date: Tue, 24 Feb 2026 01:21:44 +0200 Subject: [PATCH 01/15] hotrealod --- src/Try.Core/CompilationService.cs | 11 +-- src/Try.Core/CompileToAssemblyResult.cs | 2 + src/TryMudBlazor.Client/App.razor | 3 +- .../Models/TryConstants.cs | 1 + src/TryMudBlazor.Client/Pages/Repl.razor.cs | 20 +----- .../Pages/UserMainPage.razor | 71 +++++++++++++++++++ .../wwwroot/editor/main.js | 32 ++++++++- 7 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 src/TryMudBlazor.Client/Pages/UserMainPage.razor diff --git a/src/Try.Core/CompilationService.cs b/src/Try.Core/CompilationService.cs index 171b884a..7c66bdf5 100644 --- a/src/Try.Core/CompilationService.cs +++ b/src/Try.Core/CompilationService.cs @@ -38,12 +38,6 @@ public class CompilationService "@using MudBlazor" ]; - private const string MudBlazorServices = @" - - - -"; - // Creating the initial compilation + reading references is on the order of 250ms without caching // so making sure it doesn't happen for each run. private static CSharpCompilation _baseCompilation; @@ -129,6 +123,7 @@ public async Task CompileToAssemblyAsync( var codeHash = ComputeCodeHash(codeFiles); if (_cachedResult != null && _lastCodeHash == codeHash) { + _cachedResult.IsFromCache = true; return _cachedResult; } @@ -258,9 +253,7 @@ private async Task> CompileToCSharpAsync( { if (codeFile.Type == CodeFileType.Razor) { - var fileContent = index == 0 ? MudBlazorServices : string.Empty; - fileContent += codeFile.Content; - var projectItem = CreateRazorProjectItem(codeFile.Path, fileContent); + var projectItem = CreateRazorProjectItem(codeFile.Path, codeFile.Content); var codeDocument = projectEngine.ProcessDeclarationOnly(projectItem); var cSharpDocument = codeDocument.GetCSharpDocument(); diff --git a/src/Try.Core/CompileToAssemblyResult.cs b/src/Try.Core/CompileToAssemblyResult.cs index e1cbe298..a5379d21 100644 --- a/src/Try.Core/CompileToAssemblyResult.cs +++ b/src/Try.Core/CompileToAssemblyResult.cs @@ -10,5 +10,7 @@ public class CompileToAssemblyResult public IEnumerable Diagnostics { get; set; } = []; public byte[] AssemblyBytes { get; set; } + + public bool IsFromCache { get; set; } } } diff --git a/src/TryMudBlazor.Client/App.razor b/src/TryMudBlazor.Client/App.razor index 36693d37..4dc1a187 100644 --- a/src/TryMudBlazor.Client/App.razor +++ b/src/TryMudBlazor.Client/App.razor @@ -1,5 +1,4 @@ - + diff --git a/src/TryMudBlazor.Client/Models/TryConstants.cs b/src/TryMudBlazor.Client/Models/TryConstants.cs index 44218526..4dd073a3 100644 --- a/src/TryMudBlazor.Client/Models/TryConstants.cs +++ b/src/TryMudBlazor.Client/Models/TryConstants.cs @@ -20,6 +20,7 @@ public static class Editor public static class CodeExecution { public const string UpdateUserComponentsDll = "Try.CodeExecution.updateUserComponentsDll"; + public const string HotReloadIframe = "Try.CodeExecution.hotReloadIframe"; } } } diff --git a/src/TryMudBlazor.Client/Pages/Repl.razor.cs b/src/TryMudBlazor.Client/Pages/Repl.razor.cs index 04d5521b..647ce1a4 100644 --- a/src/TryMudBlazor.Client/Pages/Repl.razor.cs +++ b/src/TryMudBlazor.Client/Pages/Repl.razor.cs @@ -18,7 +18,6 @@ public partial class Repl : IDisposable { [Inject] private LayoutService LayoutService { get; set; } - private const string MainComponentCodePrefix = "@page \"/__main\"\n"; private const string MainUserPagePath = "/__main"; private DotNetObjectReference dotNetInstance; @@ -166,19 +165,10 @@ private async Task CompileAsync() await Task.Yield(); CompileToAssemblyResult compilationResult = null; - CodeFile mainComponent = null; - string originalMainComponentContent = null; try { this.UpdateActiveCodeFileContent(); - // Add the necessary main component code prefix and store the original content so we can revert right after compilation. - if (this.CodeFiles.TryGetValue(CoreConstants.MainComponentFilePath, out mainComponent)) - { - originalMainComponentContent = mainComponent.Content; - mainComponent.Content = MainComponentCodePrefix + originalMainComponentContent.Replace(MainComponentCodePrefix, ""); - } - compilationResult = await this.CompilationService.CompileToAssemblyAsync( this.CodeFiles.Values, this.UpdateLoaderTextAsync); @@ -193,21 +183,13 @@ private async Task CompileAsync() } finally { - if (mainComponent != null) - { - mainComponent.Content = originalMainComponentContent; - } - this.Loading = false; } if (compilationResult?.AssemblyBytes?.Length > 0) { - // Make sure the DLL is updated before reloading the user page this.JsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, compilationResult.AssemblyBytes); - - // TODO: Add error page in iframe - this.JsRuntime.InvokeVoid(Try.ReloadIframe, "user-page-window", MainUserPagePath); + this.JsRuntime.InvokeVoid(Try.CodeExecution.HotReloadIframe, "user-page-window", MainUserPagePath); } } diff --git a/src/TryMudBlazor.Client/Pages/UserMainPage.razor b/src/TryMudBlazor.Client/Pages/UserMainPage.razor new file mode 100644 index 00000000..14192b18 --- /dev/null +++ b/src/TryMudBlazor.Client/Pages/UserMainPage.razor @@ -0,0 +1,71 @@ +@page "/__main" +@using Microsoft.AspNetCore.Components +@using Microsoft.JSInterop +@using System.Runtime.Loader +@using Try.UserComponents +@inject IJSRuntime JSRuntime +@implements IDisposable + + + + + + +@code { + private Type _type = typeof(__Main); + private DotNetObjectReference _ref; + private AssemblyLoadContext _loadContext; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _ref = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("Try.registerHotReload", _ref); + } + } + + [JSInvokable] + public async Task HotReload() + { + try + { + var base64 = await JSRuntime.InvokeAsync("sessionStorage.getItem", "TryMudBlazor.UserComponentsDllBase64"); + if (base64 == null) 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(); + + // UserStartup requires DI re-registration — fall back to full iframe reload + if (assembly.GetType("UserStartup") != null || + assembly.GetType("Try.UserComponents.UserStartup") != null) + { + await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); + return; + } + + var newType = assembly.GetType("Try.UserComponents.__Main"); + if (newType != null) + { + _type = newType; + await InvokeAsync(StateHasChanged); + } + } + catch + { + await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); + } + } + + public void Dispose() + { + _ref?.Dispose(); + _loadContext?.Unload(); + } +} diff --git a/src/TryMudBlazor.Client/wwwroot/editor/main.js b/src/TryMudBlazor.Client/wwwroot/editor/main.js index d1376f65..b3320832 100644 --- a/src/TryMudBlazor.Client/wwwroot/editor/main.js +++ b/src/TryMudBlazor.Client/wwwroot/editor/main.js @@ -70,6 +70,16 @@ function throttle(func, timeFrame, id) { } } +// Listen for hot-reload signals when running inside the preview iframe +if (window.frameElement) { + window.addEventListener('message', function (e) { + if (e.origin !== window.location.origin) return; + if (e.data?.type === 'hotReload' && window._hotReloadRef) { + window._hotReloadRef.invokeMethodAsync('HotReload'); + } + }); +} + window.Try = { initialize: function (dotNetInstance) { _dotNetInstance = dotNetInstance; @@ -101,7 +111,14 @@ window.Try = { dispose: function () { _dotNetInstance = null; window.removeEventListener('keydown', onKeyDown); - } + }, + registerHotReload: function (dotNetRef) { + window._hotReloadRef = dotNetRef; + window._hotReloadReady = true; + }, + requestFullReload: function (src) { + window.parent.Try.reloadIframe('user-page-window', src); + }, } window.Try.Editor = window.Try.Editor || (function () { @@ -181,6 +198,19 @@ window.Try.CodeExecution = window.Try.CodeExecution || (function () { const USER_COMPONENTS_DLL_STORAGE_KEY = 'TryMudBlazor.UserComponentsDllBase64'; return { + hotReloadIframe: function (id, fallbackSrc) { + const iFrame = document.getElementById(id); + if (!iFrame) return; + + const iframeWindow = iFrame.contentWindow; + if (iframeWindow && iframeWindow._hotReloadReady) { + // Iframe is live — signal it to hot-reload from sessionStorage + iframeWindow.postMessage({ type: 'hotReload' }, window.location.origin); + } else { + // Iframe not yet ready (first run) — fall back to full navigation + Try.reloadIframe(id, fallbackSrc); + } + }, updateUserComponentsDll: function (dllData) { if (!dllData) return; From 5cc9d07d5c83e2433d4b27855dfc8f149fc1184a Mon Sep 17 00:00:00 2001 From: ScarletKuro Date: Tue, 24 Feb 2026 22:21:57 +0200 Subject: [PATCH 02/15] nit --- .../Pages/UserMainPage.razor | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/TryMudBlazor.Client/Pages/UserMainPage.razor b/src/TryMudBlazor.Client/Pages/UserMainPage.razor index 14192b18..e53b989a 100644 --- a/src/TryMudBlazor.Client/Pages/UserMainPage.razor +++ b/src/TryMudBlazor.Client/Pages/UserMainPage.razor @@ -1,5 +1,4 @@ @page "/__main" -@using Microsoft.AspNetCore.Components @using Microsoft.JSInterop @using System.Runtime.Loader @using Try.UserComponents @@ -13,15 +12,19 @@ @code { private Type _type = typeof(__Main); - private DotNetObjectReference _ref; private AssemblyLoadContext _loadContext; + private readonly Lazy> _ref; + + public UserMainPage() + { + _ref = new Lazy>(CreateDotNetObject()); + } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - _ref = DotNetObjectReference.Create(this); - await JSRuntime.InvokeVoidAsync("Try.registerHotReload", _ref); + await JSRuntime.InvokeVoidAsync("Try.registerHotReload", _ref.Value); } } @@ -43,15 +46,15 @@ previousContext?.Unload(); // UserStartup requires DI re-registration — fall back to full iframe reload - if (assembly.GetType("UserStartup") != null || - assembly.GetType("Try.UserComponents.UserStartup") != null) + 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 != null) + if (newType is not null) { _type = newType; await InvokeAsync(StateHasChanged); @@ -65,7 +68,12 @@ public void Dispose() { - _ref?.Dispose(); + if (_ref.IsValueCreated) + { + _ref.Value.Dispose(); + } _loadContext?.Unload(); } + + private DotNetObjectReference CreateDotNetObject() => DotNetObjectReference.Create(this); } From de128025ee4563015b34ba6907cf523da496637f Mon Sep 17 00:00:00 2001 From: ScarletKuro Date: Wed, 25 Feb 2026 10:25:16 +0200 Subject: [PATCH 03/15] Use fingerprint for blazor.webassembly.js --- src/TryMudBlazor.Client/TryMudBlazor.Client.csproj | 1 + src/TryMudBlazor.Client/wwwroot/index.html | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj b/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj index d90f063e..0a38a313 100644 --- a/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj +++ b/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj @@ -3,6 +3,7 @@ false true + true diff --git a/src/TryMudBlazor.Client/wwwroot/index.html b/src/TryMudBlazor.Client/wwwroot/index.html index 3f0ac12c..6bd2efe7 100644 --- a/src/TryMudBlazor.Client/wwwroot/index.html +++ b/src/TryMudBlazor.Client/wwwroot/index.html @@ -42,6 +42,7 @@ gtag('config', 'G-33J60J6E14'); + @@ -57,7 +58,7 @@ Reload 🗙 - + - - + + From 5f5e889bd70848d7e410401aedaef8eed12a39ab Mon Sep 17 00:00:00 2001 From: ScarletKuro Date: Fri, 10 Apr 2026 23:33:39 +0300 Subject: [PATCH 15/15] Add System.Linq.Queryable support --- src/Try.Core/CompilationService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Try.Core/CompilationService.cs b/src/Try.Core/CompilationService.cs index 7c66bdf5..1c2deba2 100644 --- a/src/Try.Core/CompilationService.cs +++ b/src/Try.Core/CompilationService.cs @@ -65,6 +65,7 @@ public static unsafe Task InitAsync() typeof(AssemblyTargetedPatchBandAttribute).Assembly, // System.Private.CoreLib typeof(NavLink).Assembly, // Microsoft.AspNetCore.Components.Web typeof(IQueryable).Assembly, // System.Linq.Expressions + typeof(Queryable).Assembly, // System.Linq.Queryable typeof(HttpClientJsonExtensions).Assembly, // System.Net.Http.Json typeof(HttpClient).Assembly, // System.Net.Http typeof(IJSRuntime).Assembly, // Microsoft.JSInterop