Skip to content

Commit acccc01

Browse files
authored
[browser] Migrate more Blazor features, prepare JavaScript API for Blazor cleanup (#87959)
* Lazy assembly loading * Satellite assembly loading * Library initializers * API cleanup * WBT for new features
1 parent 4d5d2dc commit acccc01

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1646
-196
lines changed

eng/testing/scenarios/BuildWasmAppsJobsList.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ Wasm.Build.Tests.WasmNativeDefaultsTests
2727
Wasm.Build.Tests.WasmRunOutOfAppBundleTests
2828
Wasm.Build.Tests.WasmSIMDTests
2929
Wasm.Build.Tests.WasmTemplateTests
30+
Wasm.Build.Tests.TestAppScenarios.LazyLoadingTests
31+
Wasm.Build.Tests.TestAppScenarios.LibraryInitializerTests
32+
Wasm.Build.Tests.TestAppScenarios.SatelliteLoadingTests

src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
<Reference Include="System.Net.Primitives" />
8686
<Reference Include="System.Runtime" />
8787
<Reference Include="System.Runtime.InteropServices" />
88+
<Reference Include="System.Runtime.Loader" />
8889
<Reference Include="System.Threading" />
8990
<Reference Include="System.Threading.Thread" />
9091
<Reference Include="System.Threading.Channels" />

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Generic;
5+
using System.IO;
46
using System.Reflection;
57
using System.Runtime.CompilerServices;
68
using System.Threading.Tasks;
@@ -93,6 +95,41 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer)
9395
}
9496
}
9597

98+
public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer)
99+
{
100+
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0];
101+
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];
102+
ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];
103+
try
104+
{
105+
arg_1.ToManaged(out byte[]? dllBytes);
106+
arg_2.ToManaged(out byte[]? pdbBytes);
107+
108+
if (dllBytes != null)
109+
JSHostImplementation.LoadLazyAssembly(dllBytes, pdbBytes);
110+
}
111+
catch (Exception ex)
112+
{
113+
arg_exc.ToJS(ex);
114+
}
115+
}
116+
117+
public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer)
118+
{
119+
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0];
120+
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];
121+
try
122+
{
123+
arg_1.ToManaged(out byte[]? dllBytes);
124+
125+
if (dllBytes != null)
126+
JSHostImplementation.LoadSatelliteAssembly(dllBytes);
127+
}
128+
catch (Exception ex)
129+
{
130+
arg_exc.ToJS(ex);
131+
}
132+
}
96133

97134
// The JS layer invokes this method when the JS wrapper for a JS owned object
98135
// has been collected by the JS garbage collector

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,10 @@ internal static unsafe partial class JavaScriptImports
4444
public static partial JSObject GetDotnetInstance();
4545
[JSImport("INTERNAL.dynamic_import")]
4646
public static partial Task<JSObject> DynamicImport(string moduleName, string moduleUrl);
47+
48+
#if DEBUG
49+
[JSImport("globalThis.console.log")]
50+
public static partial void Log([JSMarshalAs<JSType.String>] string message);
51+
#endif
4752
}
4853
}

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Threading.Tasks;
5-
using System.Reflection;
64
using System.Collections.Generic;
75
using System.Diagnostics.CodeAnalysis;
6+
using System.IO;
7+
using System.Reflection;
88
using System.Runtime.CompilerServices;
9+
using System.Runtime.Loader;
910
using System.Threading;
11+
using System.Threading.Tasks;
1012

1113
namespace System.Runtime.InteropServices.JavaScript
1214
{
@@ -198,6 +200,21 @@ public static JSObject CreateCSOwnedProxy(nint jsHandle)
198200
return res;
199201
}
200202

203+
[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
204+
public static void LoadLazyAssembly(byte[] dllBytes, byte[]? pdbBytes)
205+
{
206+
if (pdbBytes == null)
207+
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes));
208+
else
209+
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));
210+
}
211+
212+
[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
213+
public static void LoadSatelliteAssembly(byte[] dllBytes)
214+
{
215+
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes));
216+
}
217+
201218
#if FEATURE_WASM_THREADS
202219
public static void InstallWebWorkerInterop(bool installJSSynchronizationContext, bool isMainThread)
203220
{

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ Copyright (c) .NET Foundation. All rights reserved.
168168

169169
<Target Name="_ResolveWasmConfiguration" DependsOnTargets="_ResolveGlobalizationConfiguration">
170170
<PropertyGroup>
171-
<_TargetingNET80OrLater>$([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))</_TargetingNET80OrLater>
171+
<_TargetingNET80OrLater>false</_TargetingNET80OrLater>
172+
<_TargetingNET80OrLater Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))">true</_TargetingNET80OrLater>
172173

173174
<_BlazorEnableTimeZoneSupport>$(BlazorEnableTimeZoneSupport)</_BlazorEnableTimeZoneSupport>
174175
<_BlazorEnableTimeZoneSupport Condition="'$(_BlazorEnableTimeZoneSupport)' == ''">true</_BlazorEnableTimeZoneSupport>
@@ -180,11 +181,14 @@ Copyright (c) .NET Foundation. All rights reserved.
180181
<_WasmEnableThreads Condition="'$(_WasmEnableThreads)' == ''">false</_WasmEnableThreads>
181182

182183
<_WasmEnableWebcil>$(WasmEnableWebcil)</_WasmEnableWebcil>
183-
<_WasmEnableWebcil Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp' or '$(_TargetingNET80OrLater)' != 'true'">false</_WasmEnableWebcil>
184+
<_WasmEnableWebcil Condition="'$(_TargetingNET80OrLater)' != 'true'">false</_WasmEnableWebcil>
184185
<_WasmEnableWebcil Condition="'$(_WasmEnableWebcil)' == ''">true</_WasmEnableWebcil>
185186
<_BlazorWebAssemblyStartupMemoryCache>$(BlazorWebAssemblyStartupMemoryCache)</_BlazorWebAssemblyStartupMemoryCache>
186187
<_BlazorWebAssemblyJiterpreter>$(BlazorWebAssemblyJiterpreter)</_BlazorWebAssemblyJiterpreter>
187188
<_BlazorWebAssemblyRuntimeOptions>$(BlazorWebAssemblyRuntimeOptions)</_BlazorWebAssemblyRuntimeOptions>
189+
<_WasmDebugLevel>$(WasmDebugLevel)</_WasmDebugLevel>
190+
<_WasmDebugLevel Condition="'$(_WasmDebugLevel)' == ''">0</_WasmDebugLevel>
191+
<_WasmDebugLevel Condition="'$(_WasmDebugLevel)' == '0' and ('$(DebuggerSupport)' == 'true' or '$(Configuration)' == 'Debug')">-1</_WasmDebugLevel>
188192

189193
<!-- Workaround for https://github.com/dotnet/sdk/issues/12114-->
190194
<PublishDir Condition="'$(AppendRuntimeIdentifierToOutputPath)' != 'true' AND '$(PublishDir)' == '$(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\'">$(OutputPath)$(PublishDirName)\</PublishDir>
@@ -343,6 +347,7 @@ Copyright (c) .NET Foundation. All rights reserved.
343347
AssemblyPath="@(IntermediateAssembly)"
344348
Resources="@(_WasmOutputWithHash)"
345349
DebugBuild="true"
350+
DebugLevel="$(_WasmDebugLevel)"
346351
LinkerEnabled="false"
347352
CacheBootResources="$(BlazorCacheBootResources)"
348353
OutputPath="$(_WasmBuildBootJsonPath)"
@@ -355,7 +360,10 @@ Copyright (c) .NET Foundation. All rights reserved.
355360
StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)"
356361
Jiterpreter="$(_BlazorWebAssemblyJiterpreter)"
357362
RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)"
358-
Extensions="@(WasmBootConfigExtension)" />
363+
Extensions="@(WasmBootConfigExtension)"
364+
TargetFrameworkVersion="$(TargetFrameworkVersion)"
365+
LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)"
366+
LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" />
359367

360368
<ItemGroup>
361369
<FileWrites Include="$(_WasmBuildBootJsonPath)" />
@@ -530,6 +538,7 @@ Copyright (c) .NET Foundation. All rights reserved.
530538
AssemblyPath="@(IntermediateAssembly)"
531539
Resources="@(_WasmPublishBootResourceWithHash)"
532540
DebugBuild="false"
541+
DebugLevel="$(_WasmDebugLevel)"
533542
LinkerEnabled="$(PublishTrimmed)"
534543
CacheBootResources="$(BlazorCacheBootResources)"
535544
OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json"
@@ -542,7 +551,10 @@ Copyright (c) .NET Foundation. All rights reserved.
542551
StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)"
543552
Jiterpreter="$(_BlazorWebAssemblyJiterpreter)"
544553
RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)"
545-
Extensions="@(WasmBootConfigExtension)" />
554+
Extensions="@(WasmBootConfigExtension)"
555+
TargetFrameworkVersion="$(TargetFrameworkVersion)"
556+
LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)"
557+
LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" />
546558

547559
<ItemGroup>
548560
<FileWrites Include="$(IntermediateOutputPath)blazor.publish.boot.json" />

src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal class BrowserRunner : IAsyncDisposable
3535
public BrowserRunner(ITestOutputHelper testOutput) => _testOutput = testOutput;
3636

3737
// FIXME: options
38-
public async Task<IPage> RunAsync(ToolCommand cmd, string args, bool headless = true, Action<IConsoleMessage>? onConsoleMessage = null)
38+
public async Task<IPage> RunAsync(ToolCommand cmd, string args, bool headless = true, Action<IConsoleMessage>? onConsoleMessage = null, Func<string, string>? modifyBrowserUrl = null)
3939
{
4040
TaskCompletionSource<string> urlAvailable = new();
4141
Action<string?> outputHandler = msg =>
@@ -89,10 +89,14 @@ public async Task<IPage> RunAsync(ToolCommand cmd, string args, bool headless =
8989
Args = chromeArgs
9090
});
9191

92+
string browserUrl = urlAvailable.Task.Result;
93+
if (modifyBrowserUrl != null)
94+
browserUrl = modifyBrowserUrl(browserUrl);
95+
9296
IPage page = await Browser.NewPageAsync();
9397
if (onConsoleMessage is not null)
9498
page.Console += (_, msg) => onConsoleMessage(msg);
95-
await page.GotoAsync(urlAvailable.Task.Result);
99+
await page.GotoAsync(browserUrl);
96100
RunTask = runTask;
97101
return page;
98102
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
#nullable enable
13+
14+
namespace Wasm.Build.Tests.TestAppScenarios;
15+
16+
public class AppSettingsTests : AppTestBase
17+
{
18+
public AppSettingsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
19+
: base(output, buildContext)
20+
{
21+
}
22+
23+
[Theory]
24+
[InlineData("Development")]
25+
[InlineData("Production")]
26+
public async Task LoadAppSettingsBasedOnApplicationEnvironment(string applicationEnvironment)
27+
{
28+
CopyTestAsset("WasmBasicTestApp", "AppSettingsTests");
29+
PublishProject("Debug");
30+
31+
var result = await RunSdkStyleApp(new(
32+
Configuration: "Debug",
33+
ForPublish: true,
34+
TestScenario: "AppSettingsTest",
35+
BrowserQueryString: new Dictionary<string, string> { ["applicationEnvironment"] = applicationEnvironment }
36+
));
37+
Assert.Collection(
38+
result.TestOutput,
39+
m => Assert.Equal(GetFileExistenceMessage("/appsettings.json", true), m),
40+
m => Assert.Equal(GetFileExistenceMessage("/appsettings.Development.json", applicationEnvironment == "Development"), m),
41+
m => Assert.Equal(GetFileExistenceMessage("/appsettings.Production.json", applicationEnvironment == "Production"), m)
42+
);
43+
}
44+
45+
// Synchronize with AppSettingsTest
46+
private static string GetFileExistenceMessage(string path, bool expected) => $"'{path}' exists '{expected}'";
47+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Security.Authentication.ExtendedProtection;
9+
using System.Text;
10+
using System.Text.RegularExpressions;
11+
using System.Threading.Tasks;
12+
using Microsoft.Playwright;
13+
using Xunit.Abstractions;
14+
15+
namespace Wasm.Build.Tests.TestAppScenarios;
16+
17+
public abstract class AppTestBase : BuildTestBase
18+
{
19+
protected AppTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
20+
: base(output, buildContext)
21+
{
22+
}
23+
24+
protected string Id { get; set; }
25+
protected string LogPath { get; set; }
26+
27+
protected void CopyTestAsset(string assetName, string generatedProjectNamePrefix = null)
28+
{
29+
Id = $"{generatedProjectNamePrefix ?? assetName}_{Path.GetRandomFileName()}";
30+
InitBlazorWasmProjectDir(Id);
31+
32+
LogPath = Path.Combine(s_buildEnv.LogRootPath, Id);
33+
Utils.DirectoryCopy(Path.Combine(BuildEnvironment.TestAssetsPath, assetName), Path.Combine(_projectDir!));
34+
}
35+
36+
protected void BuildProject(string configuration)
37+
{
38+
CommandResult result = CreateDotNetCommand().ExecuteWithCapturedOutput("build", $"-bl:{GetBinLogFilePath()}", $"-p:Configuration={configuration}");
39+
result.EnsureSuccessful();
40+
}
41+
42+
protected void PublishProject(string configuration)
43+
{
44+
CommandResult result = CreateDotNetCommand().ExecuteWithCapturedOutput("publish", $"-bl:{GetBinLogFilePath()}", $"-p:Configuration={configuration}");
45+
result.EnsureSuccessful();
46+
}
47+
48+
protected string GetBinLogFilePath(string suffix = null)
49+
{
50+
if (!string.IsNullOrEmpty(suffix))
51+
suffix = "_" + suffix;
52+
53+
return Path.Combine(LogPath, $"{Id}{suffix}.binlog");
54+
}
55+
56+
protected ToolCommand CreateDotNetCommand() => new DotNetCommand(s_buildEnv, _testOutput)
57+
.WithWorkingDirectory(_projectDir!)
58+
.WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir);
59+
60+
protected async Task<RunResult> RunSdkStyleApp(RunOptions options)
61+
{
62+
string runArgs = $"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files";
63+
string workingDirectory = Path.GetFullPath(Path.Combine(FindBlazorBinFrameworkDir(options.Configuration, forPublish: options.ForPublish), ".."));
64+
65+
using var runCommand = new RunCommand(s_buildEnv, _testOutput)
66+
.WithWorkingDirectory(workingDirectory);
67+
68+
var tcs = new TaskCompletionSource<int>();
69+
70+
List<string> testOutput = new();
71+
List<string> consoleOutput = new();
72+
Regex exitRegex = new Regex("WASM EXIT (?<exitCode>[0-9]+)$");
73+
74+
await using var runner = new BrowserRunner(_testOutput);
75+
76+
IPage page = null;
77+
78+
string queryString = "?test=" + options.TestScenario;
79+
if (options.BrowserQueryString != null)
80+
queryString += "&" + string.Join("&", options.BrowserQueryString.Select(kvp => $"{kvp.Key}={kvp.Value}"));
81+
82+
page = await runner.RunAsync(runCommand, runArgs, onConsoleMessage: OnConsoleMessage, modifyBrowserUrl: url => url + queryString);
83+
84+
void OnConsoleMessage(IConsoleMessage msg)
85+
{
86+
if (EnvironmentVariables.ShowBuildOutput)
87+
Console.WriteLine($"[{msg.Type}] {msg.Text}");
88+
89+
_testOutput.WriteLine($"[{msg.Type}] {msg.Text}");
90+
consoleOutput.Add(msg.Text);
91+
92+
const string testOutputPrefix = "TestOutput -> ";
93+
if (msg.Text.StartsWith(testOutputPrefix))
94+
testOutput.Add(msg.Text.Substring(testOutputPrefix.Length));
95+
96+
var exitMatch = exitRegex.Match(msg.Text);
97+
if (exitMatch.Success)
98+
tcs.TrySetResult(int.Parse(exitMatch.Groups["exitCode"].Value));
99+
100+
if (msg.Text.StartsWith("Error: Missing test scenario"))
101+
throw new Exception(msg.Text);
102+
103+
if (options.OnConsoleMessage != null)
104+
options.OnConsoleMessage(msg, page);
105+
}
106+
107+
TimeSpan timeout = TimeSpan.FromMinutes(2);
108+
await Task.WhenAny(tcs.Task, Task.Delay(timeout));
109+
if (!tcs.Task.IsCompleted)
110+
throw new Exception($"Timed out after {timeout.TotalSeconds}s waiting for process to exit");
111+
112+
int wasmExitCode = tcs.Task.Result;
113+
if (options.ExpectedExitCode != null && wasmExitCode != options.ExpectedExitCode)
114+
throw new Exception($"Expected exit code {options.ExpectedExitCode} but got {wasmExitCode}");
115+
116+
return new(wasmExitCode, testOutput, consoleOutput);
117+
}
118+
119+
protected record RunOptions(
120+
string Configuration,
121+
string TestScenario,
122+
Dictionary<string, string> BrowserQueryString = null,
123+
bool ForPublish = false,
124+
Action<IConsoleMessage, IPage> OnConsoleMessage = null,
125+
int? ExpectedExitCode = 0
126+
);
127+
128+
protected record RunResult(
129+
int ExitCode,
130+
IReadOnlyCollection<string> TestOutput,
131+
IReadOnlyCollection<string> ConsoleOutput
132+
);
133+
}

0 commit comments

Comments
 (0)