Skip to content

Commit 3cd5321

Browse files
authored
Add support for WASM hot reload on app start (#31670)
* Add support for WASM hot reload on app start
1 parent 2e30ca1 commit 3cd5321

14 files changed

+162
-62
lines changed

src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@
1313
<property name="Scope">member</property>
1414
<property name="Target">M:Microsoft.AspNetCore.Components.BindConverter.ParserDelegateCache.MakeTypeConverterConverter``1</property>
1515
</attribute>
16-
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
17-
<argument>ILLink</argument>
18-
<argument>IL2026</argument>
19-
<property name="Scope">member</property>
20-
<property name="Target">M:Microsoft.AspNetCore.Components.LegacyRouteMatching.LegacyRouteTableFactory.&lt;&gt;c.&lt;Create&gt;b__2_1(System.Reflection.Assembly)</property>
21-
</attribute>
2216
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
2317
<argument>ILLink</argument>
2418
<argument>IL2026</argument>
@@ -49,12 +43,6 @@
4943
<property name="Scope">member</property>
5044
<property name="Target">M:Microsoft.AspNetCore.Components.DynamicComponent.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView)</property>
5145
</attribute>
52-
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
53-
<argument>ILLink</argument>
54-
<argument>IL2072</argument>
55-
<property name="Scope">member</property>
56-
<property name="Target">M:Microsoft.AspNetCore.Components.LegacyRouteMatching.LegacyRouteEntry.Match(Microsoft.AspNetCore.Components.Routing.RouteContext)</property>
57-
</attribute>
5846
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
5947
<argument>ILLink</argument>
6048
<argument>IL2072</argument>
@@ -80,4 +68,4 @@
8068
<property name="Target">M:Microsoft.AspNetCore.Components.LayoutView.&lt;&gt;c__DisplayClass13_0.&lt;WrapInLayout&gt;g__Render|0(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder)</property>
8169
</attribute>
8270
</assembly>
83-
</linker>
71+
</linker>

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -12,6 +12,7 @@
1212
<ItemGroup>
1313
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
1414
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
15+
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadEnvironment.cs" LinkBase="HotReload" />
1516
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
1617
</ItemGroup>
1718

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Platform/BootConfig.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ export class BootConfigResult {
1313
// hosts may not. Assume 'Production' in the absence of any specified value.
1414
const applicationEnvironment = environment || bootConfigResponse.headers.get('Blazor-Environment') || 'Production';
1515
const bootConfig: BootJsonData = await bootConfigResponse.json();
16+
bootConfig.modifiableAssemblies = bootConfigResponse.headers.get('DOTNET-MODIFIABLE-ASSEMBLIES');
1617

1718
return new BootConfigResult(bootConfig, applicationEnvironment);
1819
};
1920
}
2021

21-
// Keep in sync with bootJsonData in Microsoft.AspNetCore.Components.WebAssembly.Build
22+
// Keep in sync with bootJsonData from the BlazorWebAssemblySDK
2223
export interface BootJsonData {
2324
readonly entryAssembly: string;
2425
readonly resources: ResourceGroups;
@@ -28,6 +29,9 @@ export interface BootJsonData {
2829
readonly cacheBootResources: boolean;
2930
readonly config: string[];
3031
readonly icuDataMode: ICUDataMode;
32+
33+
// These properties are tacked on, and not found in the boot.json file
34+
modifiableAssemblies: string | null;
3135
}
3236

3337
export interface ResourceGroups {

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,9 +428,10 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
428428
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
429429
} catch { }
430430
MONO.mono_wasm_setenv("TZ", timeZone || 'UTC');
431-
if (resourceLoader.bootConfig.debugBuild) {
431+
432+
if (resourceLoader.bootConfig.modifiableAssemblies) {
432433
// Configure the app to enable hot reload in Development.
433-
MONO.mono_wasm_setenv('DOTNET_MODIFIABLE_ASSEMBLIES', 'debug');
434+
MONO.mono_wasm_setenv('DOTNET_MODIFIABLE_ASSEMBLIES', resourceLoader.bootConfig.modifiableAssemblies);
434435
}
435436

436437
const load_runtime = cwrap('mono_wasm_load_runtime', null, ['string', 'number']);

src/Components/WebAssembly/Server/src/ComponentsWebAssemblyApplicationBuilderExtensions.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.IO;
76
using System.Linq;
87
using System.Net.Mime;
@@ -12,6 +11,7 @@
1211
using Microsoft.AspNetCore.StaticFiles;
1312
using Microsoft.Extensions.DependencyInjection;
1413
using Microsoft.Extensions.FileProviders;
14+
using Microsoft.Extensions.Hosting;
1515
using Microsoft.Extensions.Primitives;
1616
using Microsoft.Net.Http.Headers;
1717

@@ -47,6 +47,15 @@ public static IApplicationBuilder UseBlazorFrameworkFiles(this IApplicationBuild
4747
{
4848
context.Response.Headers.Append("Blazor-Environment", webHostEnvironment.EnvironmentName);
4949

50+
// DOTNET_MODIFIABLE_ASSEMBLIES is used by the runtime to initialize hot-reload specific environment variables and is configured
51+
// by the launching process (dotnet-watch / Visual Studio).
52+
// In Development, we'll transmit the environment variable to WebAssembly as a HTTP header. The bootstrapping code will read the header
53+
// and configure it as env variable for the wasm app.
54+
if (webHostEnvironment.IsDevelopment() && Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") is not null)
55+
{
56+
context.Response.Headers.Append("DOTNET-MODIFIABLE-ASSEMBLIES", Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"));
57+
}
58+
5059
await next();
5160
});
5261

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
6-
using System.Text.Json;
75
using System.Threading;
86
using System.Threading.Tasks;
97
using Microsoft.AspNetCore.Components.Lifetime;
8+
using Microsoft.AspNetCore.Components.WebAssembly.HotReload;
109
using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure;
1110
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
1211
using Microsoft.Extensions.Configuration;
@@ -148,6 +147,13 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken)
148147

149148
await manager.RestoreStateAsync(store);
150149

150+
var initializeTask = InitializeHotReloadAsync();
151+
if (initializeTask is not null)
152+
{
153+
// The returned value will be "null" in a trimmed app
154+
await initializeTask;
155+
}
156+
151157
var tcs = new TaskCompletionSource();
152158

153159
using (cancellationToken.Register(() => tcs.TrySetResult()))
@@ -167,5 +173,11 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken)
167173
await tcs.Task;
168174
}
169175
}
176+
177+
private Task? InitializeHotReloadAsync()
178+
{
179+
// In Development scenarios, wait for hot reload to apply deltas before initiating rendering.
180+
return WebAssemblyHotReload.InitializeAsync();
181+
}
170182
}
171183
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
using System.ComponentModel;
8+
using System.Diagnostics;
9+
using System.Linq;
10+
using System.Reflection;
11+
using System.Runtime.InteropServices;
12+
using System.Threading.Tasks;
13+
using Microsoft.AspNetCore.Components.HotReload;
14+
using Microsoft.AspNetCore.Components.WebAssembly.Services;
15+
using Microsoft.JSInterop;
16+
17+
namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload
18+
{
19+
/// <summary>
20+
/// Contains methods called by interop. Intended for framework use only, not supported for use in application
21+
/// code.
22+
/// </summary>
23+
[EditorBrowsable(EditorBrowsableState.Never)]
24+
public static class WebAssemblyHotReload
25+
{
26+
private static readonly ConcurrentDictionary<Guid, List<(byte[] metadataDelta, byte[] ilDelta)>> _deltas = new();
27+
private static readonly ConcurrentDictionary<Assembly, Assembly> _appliedAssemblies = new();
28+
29+
static WebAssemblyHotReload()
30+
{
31+
if (!HotReloadEnvironment.Instance.IsHotReloadEnabled)
32+
{
33+
return;
34+
}
35+
36+
// An ApplyDelta can be called on an assembly that has not yet been loaded. This is particularly likely
37+
// when we're applying deltas on app start and child components are defined in a referenced project.
38+
// To account for this, wire up AssemblyLoad
39+
AppDomain.CurrentDomain.AssemblyLoad += (_, eventArgs) =>
40+
{
41+
var loadedAssembly = eventArgs.LoadedAssembly;
42+
var moduleId = loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId;
43+
if (moduleId is null)
44+
{
45+
return;
46+
}
47+
48+
if (_deltas.TryGetValue(moduleId.Value, out var result) && _appliedAssemblies.TryAdd(loadedAssembly, loadedAssembly))
49+
{
50+
// A delta for this specific Module exists and we haven't called ApplyUpdate on this instance of Assembly as yet.
51+
foreach (var (metadataDelta, ilDelta) in CollectionsMarshal.AsSpan(result))
52+
{
53+
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(loadedAssembly, metadataDelta, ilDelta, ReadOnlySpan<byte>.Empty);
54+
}
55+
}
56+
};
57+
}
58+
59+
internal static async Task InitializeAsync()
60+
{
61+
if (!HotReloadEnvironment.Instance.IsHotReloadEnabled)
62+
{
63+
return;
64+
}
65+
66+
var jsObjectReference = (IJSUnmarshalledObjectReference)(await DefaultWebAssemblyJSRuntime.Instance.InvokeAsync<IJSObjectReference>("import", "./_framework/blazor-hotreload.js"));
67+
await jsObjectReference.InvokeUnmarshalled<Task<int>>("receiveHotReload");
68+
}
69+
70+
/// <summary>
71+
/// For framework use only.
72+
/// </summary>
73+
[JSInvokable(nameof(ApplyHotReloadDelta))]
74+
public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDelta, byte[] ilDeta)
75+
{
76+
var moduleId = Guid.Parse(moduleIdString);
77+
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == moduleId);
78+
79+
Debug.Assert(HotReloadEnvironment.Instance.IsHotReloadEnabled);
80+
81+
if (assembly is not null)
82+
{
83+
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, metadataDelta, ilDeta, ReadOnlySpan<byte>.Empty);
84+
_appliedAssemblies.TryAdd(assembly, assembly);
85+
}
86+
87+
if (_deltas.TryGetValue(moduleId, out var deltas))
88+
{
89+
deltas.Add((metadataDelta, ilDeta));
90+
}
91+
else
92+
{
93+
_deltas[moduleId] = new List<(byte[], byte[])>(1)
94+
{
95+
(metadataDelta, ilDeta)
96+
};
97+
}
98+
99+
// Remove this once there's a runtime API to subscribe to.
100+
typeof(ComponentBase).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager")!.GetMethod("DeltaApplied", BindingFlags.Public | BindingFlags.Static)!.Invoke(null, null);
101+
}
102+
}
103+
}

src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
54
using System.ComponentModel;
6-
using System.Linq;
7-
using System.Reflection;
85
using System.Threading.Tasks;
9-
using Microsoft.AspNetCore.Components.HotReload;
106
using Microsoft.AspNetCore.Components.RenderTree;
117
using Microsoft.AspNetCore.Components.Web;
128
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
@@ -45,23 +41,5 @@ public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string even
4541
webEvent.EventFieldInfo,
4642
webEvent.EventArgs);
4743
}
48-
49-
/// <summary>
50-
/// For framework use only.
51-
/// </summary>
52-
[JSInvokable(nameof(ApplyHotReloadDelta))]
53-
public static void ApplyHotReloadDelta(string moduleId, byte[] metadataDelta, byte[] ilDeta)
54-
{
55-
var moduleIdGuid = Guid.Parse(moduleId);
56-
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == moduleIdGuid);
57-
58-
if (assembly is not null)
59-
{
60-
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, metadataDelta, ilDeta, ReadOnlySpan<byte>.Empty);
61-
}
62-
63-
// Remove this once there's a runtime API to subscribe to.
64-
typeof(ComponentBase).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager")!.GetMethod("DeltaApplied", BindingFlags.Public | BindingFlags.Static)!.Invoke(null, null);
65-
}
6644
}
6745
}

src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<argument>ILLink</argument>
2424
<argument>IL2026</argument>
2525
<property name="Scope">member</property>
26-
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])</property>
26+
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])</property>
2727
</attribute>
2828
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
2929
<argument>ILLink</argument>
@@ -53,7 +53,7 @@
5353
<argument>ILLink</argument>
5454
<argument>IL2075</argument>
5555
<property name="Scope">member</property>
56-
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])</property>
56+
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])</property>
5757
</attribute>
5858
</assembly>
5959
</linker>

src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,7 @@
1717
<Reference Include="Microsoft.Extensions.Logging" />
1818
<Reference Include="Microsoft.JSInterop.WebAssembly" />
1919

20-
<ProjectReference
21-
Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj"
22-
ReferenceOutputAssemblies="false"
23-
SkipGetTargetFrameworkProperties="true"
24-
UndefineProperties="TargetFramework"
25-
Private="false"
26-
Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
20+
<ProjectReference Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" ReferenceOutputAssemblies="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework" Private="false" Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
2721
</ItemGroup>
2822

2923
<ItemGroup>
@@ -38,6 +32,7 @@
3832
<Compile Include="$(SharedSourceRoot)Components\ComponentParameter.cs" Link="Prerendering/ComponentParameter.cs" />
3933
<Compile Include="$(SharedSourceRoot)Components\PrerenderComponentApplicationStore.cs" />
4034
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
35+
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadEnvironment.cs" LinkBase="HotReload" />
4136
</ItemGroup>
4237

4338
<ItemGroup>
@@ -48,21 +43,21 @@
4843

4944
<PropertyGroup>
5045
<!-- Microsoft.AspNetCore.Components.Web.JS.npmproj always capitalizes the directory name. -->
51-
<BlazorWebAssemblyJSFile
52-
Condition=" '$(Configuration)' == 'Debug' ">..\..\..\Web.JS\dist\Debug\blazor.webassembly.js</BlazorWebAssemblyJSFile>
53-
<BlazorWebAssemblyJSFile
54-
Condition=" '$(Configuration)' != 'Debug' ">..\..\..\Web.JS\dist\Release\blazor.webassembly.js</BlazorWebAssemblyJSFile>
46+
<BlazorWebAssemblyJSFile Condition=" '$(Configuration)' == 'Debug' ">..\..\..\Web.JS\dist\Debug\blazor.webassembly.js</BlazorWebAssemblyJSFile>
47+
<BlazorWebAssemblyJSFile Condition=" '$(Configuration)' != 'Debug' ">..\..\..\Web.JS\dist\Release\blazor.webassembly.js</BlazorWebAssemblyJSFile>
5548
</PropertyGroup>
5649

5750
<ItemGroup>
5851
<Content Include="$(BlazorWebAssemblyJSFile)" Pack="true" PackagePath="build\$(DefaultNetCoreTargetFramework)\" LinkBase="build\$(DefaultNetCoreTargetFramework)\" />
5952
<Content Include="build\$(DefaultNetCoreTargetFramework)\*.props" Pack="true" PackagePath="build\$(DefaultNetCoreTargetFramework)\" />
6053
</ItemGroup>
6154

55+
<ItemGroup>
56+
<EmbeddedResource Include="Properties\ILLink.Substitutions.xml" LogicalName="ILLink.Substitutions.xml" />
57+
</ItemGroup>
58+
6259
<!-- blazor.webassembly.js should exist after Microsoft.AspNetCore.Components.Web.JS.npmproj builds. -->
63-
<Target Name="_CheckBlazorServerJSPath" BeforeTargets="GenerateNuspec" DependsOnTargets="ResolveProjectReferences"
64-
Condition=" '$(IsPackable)' == 'true' ">
65-
<Error Text="'$(BlazorWebAssemblyJSFile)' does not exist. Enable NodeJS to pack this project."
66-
Condition=" !EXISTS('$(BlazorWebAssemblyJSFile)') " />
60+
<Target Name="_CheckBlazorServerJSPath" BeforeTargets="GenerateNuspec" DependsOnTargets="ResolveProjectReferences" Condition=" '$(IsPackable)' == 'true' ">
61+
<Error Text="'$(BlazorWebAssemblyJSFile)' does not exist. Enable NodeJS to pack this project." Condition=" !EXISTS('$(BlazorWebAssemblyJSFile)') " />
6762
</Target>
6863
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<linker>
2+
<assembly fullname="Microsoft.AspNetCore.Components.WebAssembly" >
3+
<!-- HotReload will not be available in a trimmed app. We'll attempt to aggressively remove all references to it. -->
4+
<type fullname="Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHost">
5+
<method signature="System.Threading.Tasks.Task InitializeHotReloadAsync()" body="stub" />
6+
</type>
7+
</assembly>
8+
</linker>

0 commit comments

Comments
 (0)