diff --git a/eng/testing/scenarios/BuildWasmAppsJobsList.txt b/eng/testing/scenarios/BuildWasmAppsJobsList.txt index 64f6abf5a1a65..ee53644517fe7 100644 --- a/eng/testing/scenarios/BuildWasmAppsJobsList.txt +++ b/eng/testing/scenarios/BuildWasmAppsJobsList.txt @@ -27,3 +27,6 @@ Wasm.Build.Tests.WasmNativeDefaultsTests Wasm.Build.Tests.WasmRunOutOfAppBundleTests Wasm.Build.Tests.WasmSIMDTests Wasm.Build.Tests.WasmTemplateTests +Wasm.Build.Tests.TestAppScenarios.LazyLoadingTests +Wasm.Build.Tests.TestAppScenarios.LibraryInitializerTests +Wasm.Build.Tests.TestAppScenarios.SatelliteLoadingTests diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj index eccfb146270b4..3cc3478d9f491 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj @@ -85,6 +85,7 @@ + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 5781dc2bad31b..e89142b28da04 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -93,6 +95,41 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) } } + public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) + { + ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; + ref JSMarshalerArgument arg_1 = ref arguments_buffer[2]; + ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; + try + { + arg_1.ToManaged(out byte[]? dllBytes); + arg_2.ToManaged(out byte[]? pdbBytes); + + if (dllBytes != null) + JSHostImplementation.LoadLazyAssembly(dllBytes, pdbBytes); + } + catch (Exception ex) + { + arg_exc.ToJS(ex); + } + } + + public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) + { + ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; + ref JSMarshalerArgument arg_1 = ref arguments_buffer[2]; + try + { + arg_1.ToManaged(out byte[]? dllBytes); + + if (dllBytes != null) + JSHostImplementation.LoadSatelliteAssembly(dllBytes); + } + catch (Exception ex) + { + arg_exc.ToJS(ex); + } + } // The JS layer invokes this method when the JS wrapper for a JS owned object // has been collected by the JS garbage collector diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs index 4a7b92ddb1949..589ea75add2fb 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs @@ -44,5 +44,10 @@ internal static unsafe partial class JavaScriptImports public static partial JSObject GetDotnetInstance(); [JSImport("INTERNAL.dynamic_import")] public static partial Task DynamicImport(string moduleName, string moduleUrl); + +#if DEBUG + [JSImport("globalThis.console.log")] + public static partial void Log([JSMarshalAs] string message); +#endif } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index 7947a8f2c7dbf..499c593a04d2c 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -1,12 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Threading.Tasks; -using System.Reflection; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.Loader; using System.Threading; +using System.Threading.Tasks; namespace System.Runtime.InteropServices.JavaScript { @@ -198,6 +200,21 @@ public static JSObject CreateCSOwnedProxy(nint jsHandle) return res; } + [Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")] + public static void LoadLazyAssembly(byte[] dllBytes, byte[]? pdbBytes) + { + if (pdbBytes == null) + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes)); + else + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes)); + } + + [Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")] + public static void LoadSatelliteAssembly(byte[] dllBytes) + { + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes)); + } + #if FEATURE_WASM_THREADS public static void InstallWebWorkerInterop(bool installJSSynchronizationContext, bool isMainThread) { diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index cdcf90d57502c..13d920fd52ecd 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -168,7 +168,8 @@ Copyright (c) .NET Foundation. All rights reserved. - <_TargetingNET80OrLater>$([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0')) + <_TargetingNET80OrLater>false + <_TargetingNET80OrLater Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))">true <_BlazorEnableTimeZoneSupport>$(BlazorEnableTimeZoneSupport) <_BlazorEnableTimeZoneSupport Condition="'$(_BlazorEnableTimeZoneSupport)' == ''">true @@ -180,11 +181,14 @@ Copyright (c) .NET Foundation. All rights reserved. <_WasmEnableThreads Condition="'$(_WasmEnableThreads)' == ''">false <_WasmEnableWebcil>$(WasmEnableWebcil) - <_WasmEnableWebcil Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp' or '$(_TargetingNET80OrLater)' != 'true'">false + <_WasmEnableWebcil Condition="'$(_TargetingNET80OrLater)' != 'true'">false <_WasmEnableWebcil Condition="'$(_WasmEnableWebcil)' == ''">true <_BlazorWebAssemblyStartupMemoryCache>$(BlazorWebAssemblyStartupMemoryCache) <_BlazorWebAssemblyJiterpreter>$(BlazorWebAssemblyJiterpreter) <_BlazorWebAssemblyRuntimeOptions>$(BlazorWebAssemblyRuntimeOptions) + <_WasmDebugLevel>$(WasmDebugLevel) + <_WasmDebugLevel Condition="'$(_WasmDebugLevel)' == ''">0 + <_WasmDebugLevel Condition="'$(_WasmDebugLevel)' == '0' and ('$(DebuggerSupport)' == 'true' or '$(Configuration)' == 'Debug')">-1 $(OutputPath)$(PublishDirName)\ @@ -343,6 +347,7 @@ Copyright (c) .NET Foundation. All rights reserved. AssemblyPath="@(IntermediateAssembly)" Resources="@(_WasmOutputWithHash)" DebugBuild="true" + DebugLevel="$(_WasmDebugLevel)" LinkerEnabled="false" CacheBootResources="$(BlazorCacheBootResources)" OutputPath="$(_WasmBuildBootJsonPath)" @@ -355,7 +360,10 @@ Copyright (c) .NET Foundation. All rights reserved. StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)" Jiterpreter="$(_BlazorWebAssemblyJiterpreter)" RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)" - Extensions="@(WasmBootConfigExtension)" /> + Extensions="@(WasmBootConfigExtension)" + TargetFrameworkVersion="$(TargetFrameworkVersion)" + LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)" + LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" /> @@ -530,6 +538,7 @@ Copyright (c) .NET Foundation. All rights reserved. AssemblyPath="@(IntermediateAssembly)" Resources="@(_WasmPublishBootResourceWithHash)" DebugBuild="false" + DebugLevel="$(_WasmDebugLevel)" LinkerEnabled="$(PublishTrimmed)" CacheBootResources="$(BlazorCacheBootResources)" OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json" @@ -542,7 +551,10 @@ Copyright (c) .NET Foundation. All rights reserved. StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)" Jiterpreter="$(_BlazorWebAssemblyJiterpreter)" RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)" - Extensions="@(WasmBootConfigExtension)" /> + Extensions="@(WasmBootConfigExtension)" + TargetFrameworkVersion="$(TargetFrameworkVersion)" + LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)" + LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" /> diff --git a/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs b/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs index f7be214f24f94..f12bef8ce5315 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs @@ -35,7 +35,7 @@ internal class BrowserRunner : IAsyncDisposable public BrowserRunner(ITestOutputHelper testOutput) => _testOutput = testOutput; // FIXME: options - public async Task RunAsync(ToolCommand cmd, string args, bool headless = true, Action? onConsoleMessage = null) + public async Task RunAsync(ToolCommand cmd, string args, bool headless = true, Action? onConsoleMessage = null, Func? modifyBrowserUrl = null) { TaskCompletionSource urlAvailable = new(); Action outputHandler = msg => @@ -89,10 +89,14 @@ public async Task RunAsync(ToolCommand cmd, string args, bool headless = Args = chromeArgs }); + string browserUrl = urlAvailable.Task.Result; + if (modifyBrowserUrl != null) + browserUrl = modifyBrowserUrl(browserUrl); + IPage page = await Browser.NewPageAsync(); if (onConsoleMessage is not null) page.Console += (_, msg) => onConsoleMessage(msg); - await page.GotoAsync(urlAvailable.Task.Result); + await page.GotoAsync(browserUrl); RunTask = runTask; return page; } diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppSettingsTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppSettingsTests.cs new file mode 100644 index 0000000000000..965ae20558ec3 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppSettingsTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests.TestAppScenarios; + +public class AppSettingsTests : AppTestBase +{ + public AppSettingsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Theory] + [InlineData("Development")] + [InlineData("Production")] + public async Task LoadAppSettingsBasedOnApplicationEnvironment(string applicationEnvironment) + { + CopyTestAsset("WasmBasicTestApp", "AppSettingsTests"); + PublishProject("Debug"); + + var result = await RunSdkStyleApp(new( + Configuration: "Debug", + ForPublish: true, + TestScenario: "AppSettingsTest", + BrowserQueryString: new Dictionary { ["applicationEnvironment"] = applicationEnvironment } + )); + Assert.Collection( + result.TestOutput, + m => Assert.Equal(GetFileExistenceMessage("/appsettings.json", true), m), + m => Assert.Equal(GetFileExistenceMessage("/appsettings.Development.json", applicationEnvironment == "Development"), m), + m => Assert.Equal(GetFileExistenceMessage("/appsettings.Production.json", applicationEnvironment == "Production"), m) + ); + } + + // Synchronize with AppSettingsTest + private static string GetFileExistenceMessage(string path, bool expected) => $"'{path}' exists '{expected}'"; +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs new file mode 100644 index 0000000000000..2e0907344000c --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Authentication.ExtendedProtection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Playwright; +using Xunit.Abstractions; + +namespace Wasm.Build.Tests.TestAppScenarios; + +public abstract class AppTestBase : BuildTestBase +{ + protected AppTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + protected string Id { get; set; } + protected string LogPath { get; set; } + + protected void CopyTestAsset(string assetName, string generatedProjectNamePrefix = null) + { + Id = $"{generatedProjectNamePrefix ?? assetName}_{Path.GetRandomFileName()}"; + InitBlazorWasmProjectDir(Id); + + LogPath = Path.Combine(s_buildEnv.LogRootPath, Id); + Utils.DirectoryCopy(Path.Combine(BuildEnvironment.TestAssetsPath, assetName), Path.Combine(_projectDir!)); + } + + protected void BuildProject(string configuration) + { + CommandResult result = CreateDotNetCommand().ExecuteWithCapturedOutput("build", $"-bl:{GetBinLogFilePath()}", $"-p:Configuration={configuration}"); + result.EnsureSuccessful(); + } + + protected void PublishProject(string configuration) + { + CommandResult result = CreateDotNetCommand().ExecuteWithCapturedOutput("publish", $"-bl:{GetBinLogFilePath()}", $"-p:Configuration={configuration}"); + result.EnsureSuccessful(); + } + + protected string GetBinLogFilePath(string suffix = null) + { + if (!string.IsNullOrEmpty(suffix)) + suffix = "_" + suffix; + + return Path.Combine(LogPath, $"{Id}{suffix}.binlog"); + } + + protected ToolCommand CreateDotNetCommand() => new DotNetCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(_projectDir!) + .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir); + + protected async Task RunSdkStyleApp(RunOptions options) + { + string runArgs = $"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files"; + string workingDirectory = Path.GetFullPath(Path.Combine(FindBlazorBinFrameworkDir(options.Configuration, forPublish: options.ForPublish), "..")); + + using var runCommand = new RunCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(workingDirectory); + + var tcs = new TaskCompletionSource(); + + List testOutput = new(); + List consoleOutput = new(); + Regex exitRegex = new Regex("WASM EXIT (?[0-9]+)$"); + + await using var runner = new BrowserRunner(_testOutput); + + IPage page = null; + + string queryString = "?test=" + options.TestScenario; + if (options.BrowserQueryString != null) + queryString += "&" + string.Join("&", options.BrowserQueryString.Select(kvp => $"{kvp.Key}={kvp.Value}")); + + page = await runner.RunAsync(runCommand, runArgs, onConsoleMessage: OnConsoleMessage, modifyBrowserUrl: url => url + queryString); + + void OnConsoleMessage(IConsoleMessage msg) + { + if (EnvironmentVariables.ShowBuildOutput) + Console.WriteLine($"[{msg.Type}] {msg.Text}"); + + _testOutput.WriteLine($"[{msg.Type}] {msg.Text}"); + consoleOutput.Add(msg.Text); + + const string testOutputPrefix = "TestOutput -> "; + if (msg.Text.StartsWith(testOutputPrefix)) + testOutput.Add(msg.Text.Substring(testOutputPrefix.Length)); + + var exitMatch = exitRegex.Match(msg.Text); + if (exitMatch.Success) + tcs.TrySetResult(int.Parse(exitMatch.Groups["exitCode"].Value)); + + if (msg.Text.StartsWith("Error: Missing test scenario")) + throw new Exception(msg.Text); + + if (options.OnConsoleMessage != null) + options.OnConsoleMessage(msg, page); + } + + TimeSpan timeout = TimeSpan.FromMinutes(2); + await Task.WhenAny(tcs.Task, Task.Delay(timeout)); + if (!tcs.Task.IsCompleted) + throw new Exception($"Timed out after {timeout.TotalSeconds}s waiting for process to exit"); + + int wasmExitCode = tcs.Task.Result; + if (options.ExpectedExitCode != null && wasmExitCode != options.ExpectedExitCode) + throw new Exception($"Expected exit code {options.ExpectedExitCode} but got {wasmExitCode}"); + + return new(wasmExitCode, testOutput, consoleOutput); + } + + protected record RunOptions( + string Configuration, + string TestScenario, + Dictionary BrowserQueryString = null, + bool ForPublish = false, + Action OnConsoleMessage = null, + int? ExpectedExitCode = 0 + ); + + protected record RunResult( + int ExitCode, + IReadOnlyCollection TestOutput, + IReadOnlyCollection ConsoleOutput + ); +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs new file mode 100644 index 0000000000000..022f700775ba9 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests.TestAppScenarios; + +public class LazyLoadingTests : AppTestBase +{ + public LazyLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Fact] + public async Task LoadLazyAssemblyBeforeItIsNeeded() + { + CopyTestAsset("WasmBasicTestApp", "LazyLoadingTests"); + PublishProject("Debug"); + + var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "LazyLoadingTest")); + Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON"); + } + + [Fact] + public async Task FailOnMissingLazyAssembly() + { + CopyTestAsset("WasmBasicTestApp", "LazyLoadingTests"); + PublishProject("Debug"); + + var result = await RunSdkStyleApp(new( + Configuration: "Debug", + ForPublish: true, + TestScenario: "LazyLoadingTest", + BrowserQueryString: new Dictionary { ["loadRequiredAssembly"] = "false" }, + ExpectedExitCode: 1 + )); + Assert.True(result.ConsoleOutput.Any(m => m.Contains("Could not load file or assembly") && m.Contains("System.Text.Json")), "The lazy loading test didn't emit expected error message"); + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LibraryInitializerTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LibraryInitializerTests.cs new file mode 100644 index 0000000000000..bd33d2b34cb84 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LibraryInitializerTests.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Playwright; +using Xunit.Abstractions; +using Xunit; + +#nullable enable + +namespace Wasm.Build.Tests.TestAppScenarios; + +public class LibraryInitializerTests : AppTestBase +{ + public LibraryInitializerTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Fact] + public async Task LoadLibraryInitializer() + { + CopyTestAsset("WasmBasicTestApp", "LibraryInitializerTests_LoadLibraryInitializer"); + PublishProject("Debug"); + + var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "LibraryInitializerTest")); + Assert.Collection( + result.TestOutput, + m => Assert.Equal("LIBRARY_INITIALIZER_TEST = 1", m) + ); + } + + [Fact] + public async Task AbortStartupOnError() + { + CopyTestAsset("WasmBasicTestApp", "LibraryInitializerTests_AbortStartupOnError"); + PublishProject("Debug"); + + var result = await RunSdkStyleApp(new( + Configuration: "Debug", + ForPublish: true, + TestScenario: "LibraryInitializerTest", + BrowserQueryString: new Dictionary { ["throwError"] = "true" }, + ExpectedExitCode: 1 + )); + Assert.True(result.ConsoleOutput.Any(m => m.Contains("MONO_WASM: Failed to invoke 'onRuntimeConfigLoaded' on library initializer '../WasmBasicTestApp.lib.module.js': Error: Error thrown from library initializer")), "The library initializer test didn't emit expected error message"); + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs new file mode 100644 index 0000000000000..22b41ea798dbe --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Playwright; +using Xunit.Abstractions; +using Xunit; + +#nullable enable + +namespace Wasm.Build.Tests.TestAppScenarios; + +public class SatelliteLoadingTests : AppTestBase +{ + public SatelliteLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Fact] + public async Task LoadSatelliteAssembly() + { + CopyTestAsset("WasmBasicTestApp", "SatelliteLoadingTests"); + PublishProject("Debug"); + + var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "SatelliteAssembliesTest")); + Assert.Collection( + result.TestOutput, + m => Assert.Equal("default: hello", m), + m => Assert.Equal("es-ES without satellite: hello", m), + m => Assert.Equal("default: hello", m), + m => Assert.Equal("es-ES with satellite: hola", m) + ); + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index fb09aa484eea2..c00042c7f0e79 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -42,8 +42,7 @@ - + @@ -98,8 +97,8 @@ <_RuntimePackVersions Include="$(PackageVersion)" EnvVarName="RUNTIME_PACK_VER8" /> - <_RuntimePackVersions Include="$(PackageVersionNet7)" EnvVarName="RUNTIME_PACK_VER7" Condition="'$(PackageVersionNet7)' != ''"/> - <_RuntimePackVersions Include="$(PackageVersion)" EnvVarName="RUNTIME_PACK_VER7" Condition="'$(PackageVersionNet7)' == ''"/> + <_RuntimePackVersions Include="$(PackageVersionNet7)" EnvVarName="RUNTIME_PACK_VER7" Condition="'$(PackageVersionNet7)' != ''" /> + <_RuntimePackVersions Include="$(PackageVersion)" EnvVarName="RUNTIME_PACK_VER7" Condition="'$(PackageVersionNet7)' == ''" /> <_RuntimePackVersions Include="$(PackageVersionNet6)" EnvVarName="RUNTIME_PACK_VER6" /> @@ -122,6 +121,7 @@ $(RunScriptCommand) -method $(XUnitMethodName) $(RunScriptCommand) -class $(XUnitClassName) + $(RunScriptCommand) -namespace $(XUnitNamespace) $(RunScriptCommand) -notrait category=IgnoreForCI -notrait category=failing diff --git a/src/mono/wasm/runtime/assets.ts b/src/mono/wasm/runtime/assets.ts index 8eacc484dde5f..5070a6d75853e 100644 --- a/src/mono/wasm/runtime/assets.ts +++ b/src/mono/wasm/runtime/assets.ts @@ -73,9 +73,9 @@ export function instantiate_asset(asset: AssetEntry, url: string, bytes: Uint8Ar if (asset.behavior === "assembly") { // this is reading flag inside the DLL about the existence of PDB // it doesn't relate to whether the .pdb file is downloaded at all - const hasPpdb = cwraps.mono_wasm_add_assembly(virtualName, offset!, bytes.length); + const hasPdb = cwraps.mono_wasm_add_assembly(virtualName, offset!, bytes.length); - if (!hasPpdb) { + if (!hasPdb) { const index = loaderHelpers._loaded_files.findIndex(element => element.file == virtualName); loaderHelpers._loaded_files.splice(index, 1); } diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index dcc03f913e8aa..6d55a73a3cc93 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -81,6 +81,13 @@ interface DotnetHostBuilder { withDebugging(level: number): DotnetHostBuilder; withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; withApplicationArgumentsFromQuery(): DotnetHostBuilder; + withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder; + withApplicationCulture(applicationCulture?: string): DotnetHostBuilder; + /** + * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched + * from a custom source, such as an external CDN. + */ + withResourceLoader(loadBootResource?: LoadBootResourceCallback): DotnetHostBuilder; create(): Promise; run(): Promise; } @@ -120,7 +127,7 @@ type MonoConfig = { /** * debugLevel > 0 enables debugging and sets the debug log level to debugLevel * debugLevel == 0 disables debugging and enables interpreter optimizations - * debugLevel < 0 enabled debugging and disables debug logging. + * debugLevel < 0 enables debugging and disables debug logging. */ debugLevel?: number; /** @@ -149,7 +156,56 @@ type MonoConfig = { * application environment */ applicationEnvironment?: string; + /** + * Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47 + */ + applicationCulture?: string; + /** + * definition of assets to load along with the runtime. + */ + resources?: ResourceGroups; + /** + * config extensions declared in MSBuild items @(WasmBootConfigExtension) + */ + extensions?: { + [name: string]: any; + }; +}; +type ResourceExtensions = { + [extensionName: string]: ResourceList; +}; +interface ResourceGroups { + readonly hash?: string; + readonly assembly?: ResourceList; + readonly lazyAssembly?: ResourceList; + readonly pdb?: ResourceList; + readonly runtime?: ResourceList; + readonly satelliteResources?: { + [cultureName: string]: ResourceList; + }; + readonly libraryInitializers?: ResourceList; + readonly libraryStartupModules?: { + readonly onRuntimeConfigLoaded?: ResourceList; + readonly onRuntimeReady?: ResourceList; + }; + readonly extensions?: ResourceExtensions; + readonly vfs?: { + [virtualPath: string]: ResourceList; + }; +} +type ResourceList = { + [name: string]: string; }; +/** + * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched + * from a custom source, such as an external CDN. + * @param type The type of the resource to be loaded. + * @param name The name of the resource to be loaded. + * @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute. + * @param integrity The integrity string representing the expected content in the response. + * @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior. + */ +type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; interface ResourceRequest { name: string; behavior: AssetBehaviours; @@ -189,11 +245,81 @@ interface AssetEntry extends ResourceRequest { */ pendingDownload?: LoadingResource; } -type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads" | "js-module-runtime" | "js-module-dotnet" | "js-module-native" | "symbols"; -type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". -"invariant" | // operate in invariant globalization mode. -"hybrid" | // operate in hybrid globalization mode with small ICU files, using native platform functions -"auto"; +type AssetBehaviours = +/** + * Load asset as a managed resource assembly. + */ +"resource" +/** + * Load asset as a managed assembly. + */ + | "assembly" +/** + * Load asset as a managed debugging information. + */ + | "pdb" +/** + * Store asset into the native heap. + */ + | "heap" +/** + * Load asset as an ICU data archive. + */ + | "icu" +/** + * Load asset into the virtual filesystem (for fopen, File.Open, etc). + */ + | "vfs" +/** + * The binary of the dotnet runtime. + */ + | "dotnetwasm" +/** + * The javascript module for threads. + */ + | "js-module-threads" +/** + * The javascript module for threads. + */ + | "js-module-runtime" +/** + * The javascript module for threads. + */ + | "js-module-dotnet" +/** + * The javascript module for threads. + */ + | "js-module-native" +/** + * The javascript module that came from nuget package . + */ + | "js-module-library-initializer" +/** + * The javascript module for threads. + */ + | "symbols"; +declare const enum GlobalizationMode { + /** + * Load sharded ICU data. + */ + Sharded = "sharded", + /** + * Load all ICU data. + */ + All = "all", + /** + * Operate in invariant globalization mode. + */ + Invariant = "invariant", + /** + * Use user defined icu file. + */ + Custom = "custom", + /** + * Operate in hybrid globalization mode with small ICU files, using native platform functions. + */ + Hybrid = "hybrid" +} type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; config?: MonoConfig; @@ -213,6 +339,7 @@ type APIType = { getAssemblyExports(assemblyName: string): Promise; setModuleImports(moduleName: string, moduleImports: any): void; getConfig: () => MonoConfig; + invokeLibraryInitializers: (functionName: string, args: any[]) => Promise; setHeapB32: (offset: NativePointer, value: number | boolean) => void; setHeapU8: (offset: NativePointer, value: number) => void; setHeapU16: (offset: NativePointer, value: number) => void; @@ -270,6 +397,7 @@ type ModuleAPI = { exit: (code: number, reason?: any) => void; }; type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise; +type WebAssemblyBootResourceType = "assembly" | "pdb" | "dotnetjs" | "dotnetwasm" | "globalization" | "manifest" | "configuration"; interface IDisposable { dispose(): void; @@ -300,65 +428,9 @@ declare function mono_exit(exit_code: number, reason?: any): void; declare const dotnet: DotnetHostBuilder; declare const exit: typeof mono_exit; -interface BootJsonData { - readonly entryAssembly: string; - readonly resources: ResourceGroups; - /** Gets a value that determines if this boot config was produced from a non-published build (i.e. dotnet build or dotnet run) */ - readonly debugBuild: boolean; - readonly debugLevel: number; - readonly linkerEnabled: boolean; - readonly cacheBootResources: boolean; - readonly config: string[]; - readonly icuDataMode: ICUDataMode; - readonly startupMemoryCache: boolean | undefined; - readonly runtimeOptions: string[] | undefined; - readonly environmentVariables?: { - [name: string]: string; - }; - readonly diagnosticTracing?: boolean; - readonly pthreadPoolSize: number; - modifiableAssemblies: string | null; - aspnetCoreBrowserTools: string | null; -} -type BootJsonDataExtension = { - [extensionName: string]: ResourceList; -}; -interface ResourceGroups { - readonly hash?: string; - readonly assembly: ResourceList; - readonly lazyAssembly: ResourceList; - readonly pdb?: ResourceList; - readonly runtime: ResourceList; - readonly satelliteResources?: { - [cultureName: string]: ResourceList; - }; - readonly libraryInitializers?: ResourceList; - readonly extensions?: BootJsonDataExtension; - readonly runtimeAssets: ExtendedResourceList; - readonly vfs?: { - [virtualPath: string]: ResourceList; - }; -} -type ResourceList = { - [name: string]: string; -}; -type ExtendedResourceList = { - [name: string]: { - hash: string; - behavior: string; - }; -}; -declare enum ICUDataMode { - Sharded = 0, - All = 1, - Invariant = 2, - Custom = 3, - Hybrid = 4 -} - declare global { function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined; } declare const createDotnetRuntime: CreateDotnetRuntimeType; -export { AssetEntry, BootJsonData, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ICUDataMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; +export { AssetEntry, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; diff --git a/src/mono/wasm/runtime/export-api.ts b/src/mono/wasm/runtime/export-api.ts index 1b1b997429ac8..1b4b596bdb6e3 100644 --- a/src/mono/wasm/runtime/export-api.ts +++ b/src/mono/wasm/runtime/export-api.ts @@ -8,7 +8,7 @@ import { mono_wasm_set_module_imports } from "./invoke-js"; import { getB32, getF32, getF64, getI16, getI32, getI52, getI64Big, getI8, getU16, getU32, getU52, getU8, localHeapViewF32, localHeapViewF64, localHeapViewI16, localHeapViewI32, localHeapViewI64Big, localHeapViewI8, localHeapViewU16, localHeapViewU32, localHeapViewU8, setB32, setF32, setF64, setI16, setI32, setI52, setI64Big, setI8, setU16, setU32, setU52, setU8 } from "./memory"; import { mono_run_main, mono_run_main_and_exit } from "./run"; import { mono_wasm_setenv } from "./startup"; -import { runtimeHelpers } from "./globals"; +import { loaderHelpers, runtimeHelpers } from "./globals"; export function export_api(): any { const api: APIType = { @@ -20,6 +20,7 @@ export function export_api(): any { getConfig: (): MonoConfig => { return runtimeHelpers.config; }, + invokeLibraryInitializers: loaderHelpers.invokeLibraryInitializers, setHeapB32: setB32, setHeapU8: setU8, setHeapU16: setU16, diff --git a/src/mono/wasm/runtime/exports-internal.ts b/src/mono/wasm/runtime/exports-internal.ts index 5653bec9bd691..7854b3bdd637f 100644 --- a/src/mono/wasm/runtime/exports-internal.ts +++ b/src/mono/wasm/runtime/exports-internal.ts @@ -13,6 +13,8 @@ import { mono_wasm_get_loaded_files } from "./assets"; import { jiterpreter_dump_stats } from "./jiterpreter"; import { getOptions, applyOptions } from "./jiterpreter-support"; import { mono_wasm_gc_lock, mono_wasm_gc_unlock } from "./gc-lock"; +import { loadLazyAssembly } from "./lazyLoading"; +import { loadSatelliteAssemblies } from "./satelliteAssemblies"; export function export_internal(): any { return { @@ -78,6 +80,9 @@ export function export_internal(): any { // Blazor GC Lock support mono_wasm_gc_lock, mono_wasm_gc_unlock, + + loadLazyAssembly, + loadSatelliteAssemblies }; } diff --git a/src/mono/wasm/runtime/lazyLoading.ts b/src/mono/wasm/runtime/lazyLoading.ts new file mode 100644 index 0000000000000..8402e7f37e221 --- /dev/null +++ b/src/mono/wasm/runtime/lazyLoading.ts @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals"; +import type { WebAssemblyResourceLoader } from "./loader/blazor/WebAssemblyResourceLoader"; + +export async function loadLazyAssembly(assemblyNameToLoad: string): Promise { + const resourceLoader: WebAssemblyResourceLoader = INTERNAL.resourceLoader; + const resources = resourceLoader.bootConfig.resources; + const lazyAssemblies = resources.lazyAssembly; + if (!lazyAssemblies) { + throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly."); + } + + const assemblyMarkedAsLazy = Object.prototype.hasOwnProperty.call(lazyAssemblies, assemblyNameToLoad); + if (!assemblyMarkedAsLazy) { + throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`); + } + + if (loaderHelpers.loadedAssemblies.some(f => f.includes(assemblyNameToLoad))) { + return false; + } + + const dllNameToLoad = assemblyNameToLoad; + const pdbNameToLoad = changeExtension(assemblyNameToLoad, ".pdb"); + const shouldLoadPdb = loaderHelpers.hasDebuggingEnabled(resourceLoader.bootConfig) && resources.pdb && Object.prototype.hasOwnProperty.call(lazyAssemblies, pdbNameToLoad); + + const dllBytesPromise = resourceLoader.loadResource(dllNameToLoad, loaderHelpers.locateFile(dllNameToLoad), lazyAssemblies[dllNameToLoad], "assembly").response.then(response => response.arrayBuffer()); + + let dll = null; + let pdb = null; + if (shouldLoadPdb) { + const pdbBytesPromise = await resourceLoader.loadResource(pdbNameToLoad, loaderHelpers.locateFile(pdbNameToLoad), lazyAssemblies[pdbNameToLoad], "pdb").response.then(response => response.arrayBuffer()); + const [dllBytes, pdbBytes] = await Promise.all([dllBytesPromise, pdbBytesPromise]); + + dll = new Uint8Array(dllBytes); + pdb = new Uint8Array(pdbBytes); + } else { + const dllBytes = await dllBytesPromise; + dll = new Uint8Array(dllBytes); + pdb = null; + } + + runtimeHelpers.javaScriptExports.load_lazy_assembly(dll, pdb); + return true; +} + +function changeExtension(filename: string, newExtensionWithLeadingDot: string) { + const lastDotIndex = filename.lastIndexOf("."); + if (lastDotIndex < 0) { + throw new Error(`No extension to replace in '${filename}'`); + } + + return filename.substring(0, lastDotIndex) + newExtensionWithLeadingDot; +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/loader/blazor/BootConfig.ts b/src/mono/wasm/runtime/loader/blazor/BootConfig.ts index 05defc1890135..2687b72b02933 100644 --- a/src/mono/wasm/runtime/loader/blazor/BootConfig.ts +++ b/src/mono/wasm/runtime/loader/blazor/BootConfig.ts @@ -2,17 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { BootJsonData } from "../../types/blazor"; -import type { WebAssemblyBootResourceType } from "../../types"; +import type { LoadBootResourceCallback } from "../../types"; import { loaderHelpers } from "../globals"; -export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; - export class BootConfigResult { private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) { } static fromFetchResponse(bootConfigResponse: Response, bootConfig: BootJsonData, environment: string | undefined): BootConfigResult { - const applicationEnvironment = environment || (loaderHelpers.getApplicationEnvironment && loaderHelpers.getApplicationEnvironment(bootConfigResponse)) || "Production"; + const applicationEnvironment = environment + || (loaderHelpers.getApplicationEnvironment && loaderHelpers.getApplicationEnvironment(bootConfigResponse)) + || bootConfigResponse.headers.get("Blazor-Environment") + || bootConfigResponse.headers.get("DotNet-Environment") + || "Production"; + bootConfig.modifiableAssemblies = bootConfigResponse.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES"); bootConfig.aspnetCoreBrowserTools = bootConfigResponse.headers.get("ASPNETCORE-BROWSER-TOOLS"); diff --git a/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts b/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts index 0c3208e785174..cd90ffb2b9a7e 100644 --- a/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts +++ b/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { WebAssemblyBootResourceType, WebAssemblyStartOptions } from "../../types"; +import type { LoadBootResourceCallback, WebAssemblyBootResourceType } from "../../types"; import type { BootJsonData, ResourceList } from "../../types/blazor"; +import { loaderHelpers } from "../globals"; import { toAbsoluteUri } from "./_Polyfill"; const networkFetchCacheMode = "no-cache"; @@ -15,12 +16,12 @@ export class WebAssemblyResourceLoader { private cacheLoads: { [name: string]: LoadLogEntry } = {}; - static async initAsync(bootConfig: BootJsonData, startOptions: Partial): Promise { + static async initAsync(bootConfig: BootJsonData, loadBootResource?: LoadBootResourceCallback): Promise { const cache = await getCacheToUseIfEnabled(bootConfig); - return new WebAssemblyResourceLoader(bootConfig, cache, startOptions); + return new WebAssemblyResourceLoader(bootConfig, cache, loadBootResource); } - constructor(readonly bootConfig: BootJsonData, readonly cacheIfUsed: Cache | null, readonly startOptions: Partial) { + constructor(readonly bootConfig: BootJsonData, readonly cacheIfUsed: Cache | null, readonly loadBootResource?: LoadBootResourceCallback) { } loadResources(resources: ResourceList, url: (name: string) => string, resourceType: WebAssemblyBootResourceType): LoadingResource[] { @@ -33,7 +34,12 @@ export class WebAssemblyResourceLoader { ? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash, resourceType) : this.loadResourceWithoutCaching(name, url, contentHash, resourceType); - return { name, url: toAbsoluteUri(url), response }; + const absoluteUrl = toAbsoluteUri(url); + + if (resourceType == "assembly") { + loaderHelpers.loadedAssemblies.push(absoluteUrl); + } + return { name, url: absoluteUrl, response }; } logToConsole(): void { @@ -123,8 +129,8 @@ export class WebAssemblyResourceLoader { private loadResourceWithoutCaching(name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType): Promise { // Allow developers to override how the resource is loaded - if (this.startOptions.loadBootResource) { - const customLoadResult = this.startOptions.loadBootResource(resourceType, name, url, contentHash); + if (this.loadBootResource) { + const customLoadResult = this.loadBootResource(resourceType, name, url, contentHash); if (customLoadResult instanceof Promise) { // They are supplying an entire custom response, so just use that return customLoadResult; diff --git a/src/mono/wasm/runtime/loader/blazor/_Integration.ts b/src/mono/wasm/runtime/loader/blazor/_Integration.ts index c6ded226d51a7..8e5879770618b 100644 --- a/src/mono/wasm/runtime/loader/blazor/_Integration.ts +++ b/src/mono/wasm/runtime/loader/blazor/_Integration.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { DotnetModuleInternal, MonoConfigInternal } from "../../types/internal"; -import type { AssetBehaviours, AssetEntry, LoadingResource, WebAssemblyBootResourceType, WebAssemblyStartOptions } from "../../types"; +import { GlobalizationMode, type AssetBehaviours, type AssetEntry, type LoadBootResourceCallback, type LoadingResource, type WebAssemblyBootResourceType } from "../../types"; import type { BootJsonData } from "../../types/blazor"; import { ENVIRONMENT_IS_WEB, INTERNAL, loaderHelpers } from "../globals"; @@ -15,13 +15,13 @@ import { appendUniqueQuery } from "../assets"; let resourceLoader: WebAssemblyResourceLoader; export async function loadBootConfig(config: MonoConfigInternal, module: DotnetModuleInternal) { - const bootConfigPromise = BootConfigResult.initAsync(config.startupOptions?.loadBootResource, config.applicationEnvironment); + const bootConfigPromise = BootConfigResult.initAsync(loaderHelpers.loadBootResource, config.applicationEnvironment); const bootConfigResult: BootConfigResult = await bootConfigPromise; - await initializeBootConfig(bootConfigResult, module, config.startupOptions); + await initializeBootConfig(bootConfigResult, module, loaderHelpers.loadBootResource); } -export async function initializeBootConfig(bootConfigResult: BootConfigResult, module: DotnetModuleInternal, startupOptions?: Partial) { - INTERNAL.resourceLoader = resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, startupOptions ?? {}); +export async function initializeBootConfig(bootConfigResult: BootConfigResult, module: DotnetModuleInternal, loadBootResource?: LoadBootResourceCallback) { + INTERNAL.resourceLoader = resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, loadBootResource); mapBootConfigToMonoConfig(loaderHelpers.config, bootConfigResult.applicationEnvironment); if (ENVIRONMENT_IS_WEB) { @@ -94,7 +94,16 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl moduleConfig.remoteSources = (resourceLoader.bootConfig.resources as any).remoteSources; moduleConfig.assetsHash = resourceLoader.bootConfig.resources.hash; moduleConfig.assets = assets; - moduleConfig.globalizationMode = "icu"; + moduleConfig.extensions = resourceLoader.bootConfig.extensions; + moduleConfig.resources = { + extensions: resources.extensions + }; + + // Default values (when WasmDebugLevel is not set) + // - Build (debug) => debugBuild=true & debugLevel=-1 => -1 + // - Build (release) => debugBuild=true & debugLevel=0 => 0 + // - Publish (debug) => debugBuild=false & debugLevel=-1 => 0 + // - Publish (release) => debugBuild=false & debugLevel=0 => 0 moduleConfig.debugLevel = hasDebuggingEnabled(resourceLoader.bootConfig) ? resourceLoader.bootConfig.debugLevel : 0; moduleConfig.mainAssemblyName = resourceLoader.bootConfig.entryAssembly; @@ -149,7 +158,7 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl assets.push(asset); } } - const applicationCulture = resourceLoader.startOptions.applicationCulture || (ENVIRONMENT_IS_WEB ? (navigator.languages && navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); + const applicationCulture = moduleConfig.applicationCulture || (ENVIRONMENT_IS_WEB ? (navigator.languages && navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); const icuDataResourceName = getICUResourceName(resourceLoader.bootConfig, moduleConfig, applicationCulture); let hasIcuData = false; for (const name in resources.runtime) { @@ -196,10 +205,11 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl for (let i = 0; i < resourceLoader.bootConfig.config.length; i++) { const config = resourceLoader.bootConfig.config[i]; - if (config === "appsettings.json" || config === `appsettings.${applicationEnvironment}.json`) { + const configFileName = fileName(config); + if (configFileName === "appsettings.json" || configFileName === `appsettings.${applicationEnvironment}.json`) { assets.push({ - name: config, - resolvedUrl: appendUniqueQuery((document ? document.baseURI : "/") + config, "vfs"), + name: configFileName, + resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(config), "vfs"), behavior: "vfs", }); } @@ -219,7 +229,7 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl } if (!hasIcuData) { - moduleConfig.globalizationMode = "invariant"; + moduleConfig.globalizationMode = GlobalizationMode.Invariant; } if (resourceLoader.bootConfig.modifiableAssemblies) { @@ -227,9 +237,14 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl environmentVariables["DOTNET_MODIFIABLE_ASSEMBLIES"] = resourceLoader.bootConfig.modifiableAssemblies; } - if (resourceLoader.startOptions.applicationCulture) { + if (resourceLoader.bootConfig.aspnetCoreBrowserTools) { + // See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000 + environmentVariables["__ASPNETCORE_BROWSER_TOOLS"] = resourceLoader.bootConfig.aspnetCoreBrowserTools; + } + + if (moduleConfig.applicationCulture) { // If a culture is specified via start options use that to initialize the Emscripten \ .NET culture. - environmentVariables["LANG"] = `${resourceLoader.startOptions.applicationCulture}.UTF-8`; + environmentVariables["LANG"] = `${moduleConfig.applicationCulture}.UTF-8`; } if (resourceLoader.bootConfig.startupMemoryCache !== undefined) { @@ -241,30 +256,39 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl } } +function fileName(name: string) { + let lastIndexOfSlash = name.lastIndexOf("/"); + if (lastIndexOfSlash >= 0) { + lastIndexOfSlash++; + } + return name.substring(lastIndexOfSlash); +} + function getICUResourceName(bootConfig: BootJsonData, moduleConfig: MonoConfigInternal, culture: string | undefined): string { if (bootConfig.icuDataMode === ICUDataMode.Custom) { const icuFiles = Object .keys(bootConfig.resources.runtime) .filter(n => n.startsWith("icudt") && n.endsWith(".dat")); if (icuFiles.length === 1) { - moduleConfig.globalizationMode = "icu"; + moduleConfig.globalizationMode = GlobalizationMode.Custom; const customIcuFile = icuFiles[0]; return customIcuFile; } } if (bootConfig.icuDataMode === ICUDataMode.Hybrid) { - moduleConfig.globalizationMode = "hybrid"; + moduleConfig.globalizationMode = GlobalizationMode.Hybrid; const reducedICUResourceName = "icudt_hybrid.dat"; return reducedICUResourceName; } if (!culture || bootConfig.icuDataMode === ICUDataMode.All) { - moduleConfig.globalizationMode = "icu"; + moduleConfig.globalizationMode = GlobalizationMode.All; const combinedICUResourceName = "icudt.dat"; return combinedICUResourceName; } + moduleConfig.globalizationMode = GlobalizationMode.Sharded; const prefix = culture.split("-")[0]; if (prefix === "en" || [ diff --git a/src/mono/wasm/runtime/loader/config.ts b/src/mono/wasm/runtime/loader/config.ts index e8ccaead6cb69..fc728d7e7974b 100644 --- a/src/mono/wasm/runtime/loader/config.ts +++ b/src/mono/wasm/runtime/loader/config.ts @@ -9,6 +9,7 @@ import { initializeBootConfig, loadBootConfig } from "./blazor/_Integration"; import { BootConfigResult } from "./blazor/BootConfig"; import { BootJsonData } from "../types/blazor"; import { mono_log_error, mono_log_debug } from "./logging"; +import { invokeLibraryInitializers } from "./libraryInitializers"; export function deep_merge_config(target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { const providedConfig: MonoConfigInternal = { ...source }; @@ -18,9 +19,6 @@ export function deep_merge_config(target: MonoConfigInternal, source: MonoConfig if (providedConfig.environmentVariables) { providedConfig.environmentVariables = { ...(target.environmentVariables || {}), ...(providedConfig.environmentVariables || {}) }; } - if (providedConfig.startupOptions) { - providedConfig.startupOptions = { ...(target.startupOptions || {}), ...(providedConfig.startupOptions || {}) }; - } if (providedConfig.runtimeOptions) { providedConfig.runtimeOptions = [...(target.runtimeOptions || []), ...(providedConfig.runtimeOptions || [])]; } @@ -44,7 +42,6 @@ export function normalizeConfig() { config.environmentVariables = config.environmentVariables || {}; config.assets = config.assets || []; config.runtimeOptions = config.runtimeOptions || []; - config.globalizationMode = config.globalizationMode || "auto"; if (config.debugLevel === undefined && BuildConfiguration === "Debug") { config.debugLevel = -1; @@ -81,9 +78,7 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi } mono_log_debug("mono_wasm_load_config"); try { - loaderHelpers.config.applicationEnvironment = loaderHelpers.config.applicationEnvironment ?? loaderHelpers.config.startupOptions?.environment ?? "Production"; - - if (loaderHelpers.config.startupOptions && loaderHelpers.config.startupOptions.loadBootResource) { + if (loaderHelpers.loadBootResource) { // If we have custom loadBootResource await loadBootConfig(loaderHelpers.config, module); } else { @@ -94,7 +89,7 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi if (loadedAnyConfig.resources) { // If we found boot config schema normalizeConfig(); - await initializeBootConfig(BootConfigResult.fromFetchResponse(configResponse, loadedAnyConfig as BootJsonData, loaderHelpers.config.applicationEnvironment), module, loaderHelpers.config.startupOptions); + await initializeBootConfig(BootConfigResult.fromFetchResponse(configResponse, loadedAnyConfig as BootJsonData, loaderHelpers.config.applicationEnvironment), module, loaderHelpers.loadBootResource); } else { // Otherwise we found mono config schema const loadedConfig = loadedAnyConfig as MonoConfigInternal; @@ -106,6 +101,8 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi normalizeConfig(); + await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "onRuntimeConfigLoaded"); + if (module.onConfigLoaded) { try { await module.onConfigLoaded(loaderHelpers.config, exportedRuntimeAPI); diff --git a/src/mono/wasm/runtime/loader/globals.ts b/src/mono/wasm/runtime/loader/globals.ts index 6cebb54eaa428..2c7ae35f76192 100644 --- a/src/mono/wasm/runtime/loader/globals.ts +++ b/src/mono/wasm/runtime/loader/globals.ts @@ -7,6 +7,8 @@ import { abort_startup, mono_exit } from "./exit"; import { assertIsControllablePromise, createPromiseController, getPromiseController } from "./promise-controller"; import { mono_download_assets, resolve_asset_path } from "./assets"; import { setup_proxy_console } from "./logging"; +import { hasDebuggingEnabled } from "./blazor/_Polyfill"; +import { invokeLibraryInitializers } from "./libraryInitializers"; export const ENVIRONMENT_IS_NODE = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string"; export const ENVIRONMENT_IS_WEB = typeof window == "object"; @@ -43,7 +45,8 @@ export function setLoaderGlobals( exportedRuntimeAPI = globalObjects.api; INTERNAL = globalObjects.internal; Object.assign(exportedRuntimeAPI, { - INTERNAL + INTERNAL, + invokeLibraryInitializers }); Object.assign(globalObjects.module, { @@ -54,7 +57,7 @@ export function setLoaderGlobals( mono_wasm_bindings_is_ready: false, javaScriptExports: {} as any, config: globalObjects.module.config, - diagnosticTracing: false, + diagnosticTracing: false }); Object.assign(loaderHelpers, { config: globalObjects.module.config, @@ -65,6 +68,7 @@ export function setLoaderGlobals( _loaded_files: [], loadedFiles: [], + loadedAssemblies: [], actual_downloaded_assets_count: 0, actual_instantiated_assets_count: 0, expected_downloaded_assets_count: 0, @@ -84,5 +88,8 @@ export function setLoaderGlobals( resolve_asset_path, setup_proxy_console, + hasDebuggingEnabled, + invokeLibraryInitializers, + } as Partial); } diff --git a/src/mono/wasm/runtime/loader/icu.ts b/src/mono/wasm/runtime/loader/icu.ts index 74990738b7266..bffc1e3dd1346 100644 --- a/src/mono/wasm/runtime/loader/icu.ts +++ b/src/mono/wasm/runtime/loader/icu.ts @@ -1,17 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { GlobalizationMode } from "../types"; import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; import { mono_log_info, mono_log_debug } from "./logging"; export function init_globalization() { - loaderHelpers.invariantMode = loaderHelpers.config.globalizationMode === "invariant"; + loaderHelpers.invariantMode = loaderHelpers.config.globalizationMode == GlobalizationMode.Invariant; loaderHelpers.preferredIcuAsset = get_preferred_icu_asset(); if (!loaderHelpers.invariantMode) { if (loaderHelpers.preferredIcuAsset) { mono_log_debug("ICU data archive(s) available, disabling invariant mode"); - } else if (loaderHelpers.config.globalizationMode !== "icu") { + } else if (loaderHelpers.config.globalizationMode !== GlobalizationMode.Custom && loaderHelpers.config.globalizationMode !== GlobalizationMode.All && loaderHelpers.config.globalizationMode !== GlobalizationMode.Sharded) { mono_log_debug("ICU data archive(s) not available, using invariant globalization mode"); loaderHelpers.invariantMode = true; loaderHelpers.preferredIcuAsset = null; @@ -25,7 +26,7 @@ export function init_globalization() { const invariantEnv = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; const hybridEnv = "DOTNET_SYSTEM_GLOBALIZATION_HYBRID"; const env_variables = loaderHelpers.config.environmentVariables!; - if (env_variables[hybridEnv] === undefined && loaderHelpers.config.globalizationMode === "hybrid") { + if (env_variables[hybridEnv] === undefined && loaderHelpers.config.globalizationMode === GlobalizationMode.Hybrid) { env_variables[hybridEnv] = "1"; } else if (env_variables[invariantEnv] === undefined && loaderHelpers.invariantMode) { diff --git a/src/mono/wasm/runtime/loader/libraryInitializers.ts b/src/mono/wasm/runtime/loader/libraryInitializers.ts new file mode 100644 index 0000000000000..f9f5b472aed5e --- /dev/null +++ b/src/mono/wasm/runtime/loader/libraryInitializers.ts @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { mono_log_warn } from "./logging"; +import { MonoConfig } from "../types"; +import { appendUniqueQuery } from "./assets"; +import { loaderHelpers } from "./globals"; +import { abort_startup } from "./exit"; + +export type LibraryInitializerTypes = + "onRuntimeConfigLoaded" + | "onRuntimeReady"; + +async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitializerTypes): Promise { + if (!loaderHelpers.libraryInitializers) { + loaderHelpers.libraryInitializers = []; + } + + const libraryInitializers = type == "onRuntimeConfigLoaded" + ? config.resources?.libraryStartupModules?.onRuntimeConfigLoaded + : config.resources?.libraryStartupModules?.onRuntimeReady; + + if (!libraryInitializers) { + return; + } + + const initializerFiles = Object.keys(libraryInitializers); + await Promise.all(initializerFiles.map(f => importInitializer(f))); + + async function importInitializer(path: string): Promise { + try { + const adjustedPath = appendUniqueQuery(loaderHelpers.locateFile(path), "js-module-library-initializer"); + const initializer = await import(/* webpackIgnore: true */ adjustedPath); + + loaderHelpers.libraryInitializers!.push({ scriptName: path, exports: initializer }); + } catch (error) { + mono_log_warn(`Failed to import library initializer '${path}': ${error}`); + } + } +} + +export async function invokeLibraryInitializers(functionName: string, args: any[], type?: LibraryInitializerTypes) { + if (type) { + await fetchLibraryInitializers(loaderHelpers.config, type); + } + + if (!loaderHelpers.libraryInitializers) { + return; + } + + const promises = []; + for (let i = 0; i < loaderHelpers.libraryInitializers.length; i++) { + const initializer = loaderHelpers.libraryInitializers[i]; + if (initializer.exports[functionName]) { + promises.push(abortStartupOnError(initializer.scriptName, functionName, () => initializer.exports[functionName](...args))); + } + } + + await Promise.all(promises); +} + +async function abortStartupOnError(scriptName: string, methodName: string, callback: () => Promise | undefined): Promise { + try { + await callback(); + } catch (error) { + mono_log_warn(`Failed to invoke '${methodName}' on library initializer '${scriptName}': ${error}`); + abort_startup(error, true); + } +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/loader/polyfills.ts b/src/mono/wasm/runtime/loader/polyfills.ts index 0b5e2be1967f8..eed7a45fff718 100644 --- a/src/mono/wasm/runtime/loader/polyfills.ts +++ b/src/mono/wasm/runtime/loader/polyfills.ts @@ -4,6 +4,15 @@ import { INTERNAL, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, loaderHelpers, ENV let node_fs: any | undefined = undefined; let node_url: any | undefined = undefined; +const URLPolyfill = class URL { + private url; + constructor(url: string) { + this.url = url; + } + toString() { + return this.url; + } +}; export async function detect_features_and_polyfill(module: DotnetModuleInternal): Promise { @@ -15,6 +24,10 @@ export async function detect_features_and_polyfill(module: DotnetModuleInternal) loaderHelpers.scriptUrl = normalizeFileUrl(scriptUrlQuery); loaderHelpers.scriptDirectory = normalizeDirectoryUrl(loaderHelpers.scriptUrl); loaderHelpers.locateFile = (path) => { + if ("URL" in globalThis && globalThis.URL !== (URLPolyfill as any)) { + return new URL(path, loaderHelpers.scriptDirectory).toString(); + } + if (isPathAbsolute(path)) return path; return loaderHelpers.scriptDirectory + path; }; @@ -47,15 +60,7 @@ export async function detect_features_and_polyfill(module: DotnetModuleInternal) } if (typeof globalThis.URL === "undefined") { - globalThis.URL = class URL { - private url; - constructor(url: string) { - this.url = url; - } - toString() { - return this.url; - } - } as any; + globalThis.URL = URLPolyfill as any; } } diff --git a/src/mono/wasm/runtime/loader/run.ts b/src/mono/wasm/runtime/loader/run.ts index 6d8b480554bb4..04441af81d773 100644 --- a/src/mono/wasm/runtime/loader/run.ts +++ b/src/mono/wasm/runtime/loader/run.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, WebAssemblyStartOptions } from "../types"; +import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, WebAssemblyStartOptions, LoadBootResourceCallback } from "../types"; import type { MonoConfigInternal, EmscriptenModuleInternal, RuntimeModuleExportsInternal, NativeModuleExportsInternal, } from "../types/internal"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB, exportedRuntimeAPI, globalObjectsRoot } from "./globals"; @@ -13,6 +13,7 @@ import { detect_features_and_polyfill } from "./polyfills"; import { runtimeHelpers, loaderHelpers } from "./globals"; import { init_globalization } from "./icu"; import { setupPreloadChannelToMainThread } from "./worker"; +import { invokeLibraryInitializers } from "./libraryInitializers"; const module = globalObjectsRoot.module; @@ -276,10 +277,44 @@ export class HostBuilder implements DotnetHostBuilder { } withStartupOptions(startupOptions: Partial): DotnetHostBuilder { - deep_merge_config(monoConfig, { - startupOptions - }); - return this; + return this + .withApplicationEnvironment(startupOptions.environment) + .withApplicationCulture(startupOptions.applicationCulture) + .withResourceLoader(startupOptions.loadBootResource); + } + + withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder { + try { + deep_merge_config(monoConfig, { + applicationEnvironment, + }); + return this; + } catch (err) { + mono_exit(1, err); + throw err; + } + } + + withApplicationCulture(applicationCulture?: string): DotnetHostBuilder { + try { + deep_merge_config(monoConfig, { + applicationCulture, + }); + return this; + } catch (err) { + mono_exit(1, err); + throw err; + } + } + + withResourceLoader(loadBootResource?: LoadBootResourceCallback): DotnetHostBuilder { + try { + loaderHelpers.loadBootResource = loadBootResource; + return this; + } catch (err) { + mono_exit(1, err); + throw err; + } } async create(): Promise { @@ -409,12 +444,15 @@ async function createEmscriptenMain(): Promise { }); init_globalization(); + // TODO call mono_download_assets(); here in parallel ? const es6Modules = await Promise.all(promises); initializeModules(es6Modules as any); await runtimeHelpers.dotnetReady.promise; + await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "onRuntimeReady"); + return exportedRuntimeAPI; } diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 6bd65eb2ec025..e23edddfa19f3 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -8,7 +8,7 @@ import cwraps from "./cwraps"; import { runtimeHelpers, Module } from "./globals"; import { alloc_stack_frame, get_arg, get_arg_gc_handle, set_arg_type, set_gc_handle } from "./marshal"; import { invoke_method_and_handle_exception } from "./invoke-cs"; -import { marshal_array_to_cs_impl, marshal_exception_to_cs, marshal_intptr_to_cs } from "./marshal-to-cs"; +import { marshal_array_to_cs, marshal_array_to_cs_impl, marshal_exception_to_cs, marshal_intptr_to_cs } from "./marshal-to-cs"; import { marshal_int32_to_js, marshal_string_to_js, marshal_task_to_js } from "./marshal-to-js"; export function init_managed_exports(): void { @@ -37,6 +37,10 @@ export function init_managed_exports(): void { mono_assert(call_delegate_method, "Can't find CallDelegate method"); const get_managed_stack_trace_method = get_method("GetManagedStackTrace"); mono_assert(get_managed_stack_trace_method, "Can't find GetManagedStackTrace method"); + const load_satellite_assembly_method = get_method("LoadSatelliteAssembly"); + mono_assert(load_satellite_assembly_method, "Can't find LoadSatelliteAssembly method"); + const load_lazy_assembly_method = get_method("LoadLazyAssembly"); + mono_assert(load_lazy_assembly_method, "Can't find LoadLazyAssembly method"); runtimeHelpers.javaScriptExports.call_entry_point = async (entry_point: MonoMethod, program_args?: string[]): Promise => { const sp = Module.stackSave(); @@ -62,6 +66,33 @@ export function init_managed_exports(): void { Module.stackRestore(sp); } }; + runtimeHelpers.javaScriptExports.load_satellite_assembly = (dll: Uint8Array): void => { + const sp = Module.stackSave(); + try { + const args = alloc_stack_frame(3); + const arg1 = get_arg(args, 2); + set_arg_type(arg1, MarshalerType.Array); + marshal_array_to_cs(arg1, dll, MarshalerType.Byte); + invoke_method_and_handle_exception(load_satellite_assembly_method, args); + } finally { + Module.stackRestore(sp); + } + }; + runtimeHelpers.javaScriptExports.load_lazy_assembly = (dll: Uint8Array, pdb: Uint8Array | null): void => { + const sp = Module.stackSave(); + try { + const args = alloc_stack_frame(4); + const arg1 = get_arg(args, 2); + const arg2 = get_arg(args, 3); + set_arg_type(arg1, MarshalerType.Array); + set_arg_type(arg2, MarshalerType.Array); + marshal_array_to_cs(arg1, dll, MarshalerType.Byte); + marshal_array_to_cs(arg2, pdb, MarshalerType.Byte); + invoke_method_and_handle_exception(load_lazy_assembly_method, args); + } finally { + Module.stackRestore(sp); + } + }; runtimeHelpers.javaScriptExports.release_js_owned_object_by_gc_handle = (gc_handle: GCHandle) => { mono_assert(gc_handle, "Must be valid gc_handle"); const sp = Module.stackSave(); diff --git a/src/mono/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts index 0c22c590b79df..a7b3e2346cd9a 100644 --- a/src/mono/wasm/runtime/marshal-to-cs.ts +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -25,7 +25,7 @@ import { mono_log_warn } from "./logging"; export function initialize_marshalers_to_cs(): void { if (js_to_cs_marshalers.size == 0) { - js_to_cs_marshalers.set(MarshalerType.Array, _marshal_array_to_cs); + js_to_cs_marshalers.set(MarshalerType.Array, marshal_array_to_cs); js_to_cs_marshalers.set(MarshalerType.Span, _marshal_span_to_cs); js_to_cs_marshalers.set(MarshalerType.ArraySegment, _marshal_array_segment_to_cs); js_to_cs_marshalers.set(MarshalerType.Boolean, _marshal_bool_to_cs); @@ -43,7 +43,7 @@ export function initialize_marshalers_to_cs(): void { js_to_cs_marshalers.set(MarshalerType.String, _marshal_string_to_cs); js_to_cs_marshalers.set(MarshalerType.Exception, marshal_exception_to_cs); js_to_cs_marshalers.set(MarshalerType.JSException, marshal_exception_to_cs); - js_to_cs_marshalers.set(MarshalerType.JSObject, _marshal_js_object_to_cs); + js_to_cs_marshalers.set(MarshalerType.JSObject, marshal_js_object_to_cs); js_to_cs_marshalers.set(MarshalerType.Object, _marshal_cs_object_to_cs); js_to_cs_marshalers.set(MarshalerType.Task, _marshal_task_to_cs); js_to_cs_marshalers.set(MarshalerType.Action, _marshal_function_to_cs); @@ -363,7 +363,7 @@ export function marshal_exception_to_cs(arg: JSMarshalerArgument, value: any): v } } -function _marshal_js_object_to_cs(arg: JSMarshalerArgument, value: any): void { +export function marshal_js_object_to_cs(arg: JSMarshalerArgument, value: any): void { if (value === undefined || value === null) { set_arg_type(arg, MarshalerType.None); } @@ -464,12 +464,12 @@ function _marshal_cs_object_to_cs(arg: JSMarshalerArgument, value: any): void { } } -function _marshal_array_to_cs(arg: JSMarshalerArgument, value: Array | TypedArray, element_type?: MarshalerType): void { +export function marshal_array_to_cs(arg: JSMarshalerArgument, value: Array | TypedArray | undefined | null, element_type?: MarshalerType): void { mono_assert(!!element_type, "Expected valid element_type parameter"); marshal_array_to_cs_impl(arg, value, element_type); } -export function marshal_array_to_cs_impl(arg: JSMarshalerArgument, value: Array | TypedArray | undefined, element_type: MarshalerType): void { +export function marshal_array_to_cs_impl(arg: JSMarshalerArgument, value: Array | TypedArray | undefined | null, element_type: MarshalerType): void { if (value === null || value === undefined) { set_arg_type(arg, MarshalerType.None); } @@ -502,7 +502,7 @@ export function marshal_array_to_cs_impl(arg: JSMarshalerArgument, value: Array< _zero_region(buffer_ptr, buffer_length); for (let index = 0; index < length; index++) { const element_arg = get_arg(buffer_ptr, index); - _marshal_js_object_to_cs(element_arg, value[index]); + marshal_js_object_to_cs(element_arg, value[index]); } } else if (element_type == MarshalerType.Byte) { diff --git a/src/mono/wasm/runtime/satelliteAssemblies.ts b/src/mono/wasm/runtime/satelliteAssemblies.ts new file mode 100644 index 0000000000000..612d757efe859 --- /dev/null +++ b/src/mono/wasm/runtime/satelliteAssemblies.ts @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals"; +import type { WebAssemblyResourceLoader } from "./loader/blazor/WebAssemblyResourceLoader"; +import { LoadingResource } from "./types"; + +export async function loadSatelliteAssemblies(culturesToLoad: string[]): Promise { + const resourceLoader: WebAssemblyResourceLoader = INTERNAL.resourceLoader; + const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources; + if (!satelliteResources) { + return; + } + + await Promise.all(culturesToLoad! + .filter(culture => Object.prototype.hasOwnProperty.call(satelliteResources, culture)) + .map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => loaderHelpers.locateFile(fileName), "assembly")) + .reduce((previous, next) => previous.concat(next), new Array()) + .map(async resource => { + const response = await resource.response; + const bytes = await response.arrayBuffer(); + runtimeHelpers.javaScriptExports.load_satellite_assembly(new Uint8Array(bytes)); + })); +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 7faea31a50e53..728fe763c8d80 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -262,7 +262,7 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); - if (runtimeHelpers.config.startupOptions && INTERNAL.resourceLoader) { + if (INTERNAL.resourceLoader) { if (INTERNAL.resourceLoader.bootConfig.debugBuild && INTERNAL.resourceLoader.bootConfig.cacheBootResources) { INTERNAL.resourceLoader.logToConsole(); } diff --git a/src/mono/wasm/runtime/types/blazor.ts b/src/mono/wasm/runtime/types/blazor.ts index 20cfb1a97c0cf..205bac41d7e19 100644 --- a/src/mono/wasm/runtime/types/blazor.ts +++ b/src/mono/wasm/runtime/types/blazor.ts @@ -21,6 +21,8 @@ export interface BootJsonData { // These properties are tacked on, and not found in the boot.json file modifiableAssemblies: string | null; aspnetCoreBrowserTools: string | null; + + readonly extensions?: { [name: string]: any }; } export type BootJsonDataExtension = { [extensionName: string]: ResourceList }; @@ -33,6 +35,7 @@ export interface ResourceGroups { readonly runtime: ResourceList; readonly satelliteResources?: { [cultureName: string]: ResourceList }; readonly libraryInitializers?: ResourceList, + readonly libraryStartupModules?: { onRuntimeConfigLoaded: ResourceList, onRuntimeReady: ResourceList }, readonly extensions?: BootJsonDataExtension readonly runtimeAssets: ExtendedResourceList; readonly vfs?: { [virtualPath: string]: ResourceList }; diff --git a/src/mono/wasm/runtime/types/export-types.ts b/src/mono/wasm/runtime/types/export-types.ts index 379b817aa9585..aa7f7e09d2db0 100644 --- a/src/mono/wasm/runtime/types/export-types.ts +++ b/src/mono/wasm/runtime/types/export-types.ts @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { IMemoryView } from "../marshal"; -import type { CreateDotnetRuntimeType, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest } from "."; +import type { CreateDotnetRuntimeType, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode } from "."; import type { EmscriptenModule } from "./emscripten"; import type { dotnet, exit } from "../loader/index"; -import type { BootJsonData, ICUDataMode } from "./blazor"; // ----------------------------------------------------------- // this files has all public exports from the dotnet.js module @@ -22,6 +21,6 @@ export default createDotnetRuntime; export { EmscriptenModule, - RuntimeAPI, ModuleAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, BootJsonData, ICUDataMode, + RuntimeAPI, ModuleAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode, dotnet, exit }; diff --git a/src/mono/wasm/runtime/types/index.ts b/src/mono/wasm/runtime/types/index.ts index 2979576f03b30..2a6aac14f2c66 100644 --- a/src/mono/wasm/runtime/types/index.ts +++ b/src/mono/wasm/runtime/types/index.ts @@ -14,6 +14,14 @@ export interface DotnetHostBuilder { withDebugging(level: number): DotnetHostBuilder withMainAssembly(mainAssemblyName: string): DotnetHostBuilder withApplicationArgumentsFromQuery(): DotnetHostBuilder + withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder; + withApplicationCulture(applicationCulture?: string): DotnetHostBuilder; + + /** + * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched + * from a custom source, such as an external CDN. + */ + withResourceLoader(loadBootResource?: LoadBootResourceCallback): DotnetHostBuilder; create(): Promise run(): Promise } @@ -54,7 +62,7 @@ export type MonoConfig = { /** * debugLevel > 0 enables debugging and sets the debug log level to debugLevel * debugLevel == 0 disables debugging and enables interpreter optimizations - * debugLevel < 0 enabled debugging and disables debug logging. + * debugLevel < 0 enables debugging and disables debug logging. */ debugLevel?: number, /** @@ -83,8 +91,54 @@ export type MonoConfig = { * application environment */ applicationEnvironment?: string, + + /** + * Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47 + */ + applicationCulture?: string, + + /** + * definition of assets to load along with the runtime. + */ + resources?: ResourceGroups; + + /** + * config extensions declared in MSBuild items @(WasmBootConfigExtension) + */ + extensions?: { [name: string]: any }; }; +export type ResourceExtensions = { [extensionName: string]: ResourceList }; + +export interface ResourceGroups { + readonly hash?: string; + readonly assembly?: ResourceList; // nullable only temporarily + readonly lazyAssembly?: ResourceList; // nullable only temporarily + readonly pdb?: ResourceList; + readonly runtime?: ResourceList; // nullable only temporarily + readonly satelliteResources?: { [cultureName: string]: ResourceList }; + readonly libraryInitializers?: ResourceList, + readonly libraryStartupModules?: { + readonly onRuntimeConfigLoaded?: ResourceList, + readonly onRuntimeReady?: ResourceList + }, + readonly extensions?: ResourceExtensions + readonly vfs?: { [virtualPath: string]: ResourceList }; +} + +export type ResourceList = { [name: string]: string }; + +/** + * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched + * from a custom source, such as an external CDN. + * @param type The type of the resource to be loaded. + * @param name The name of the resource to be loaded. + * @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute. + * @param integrity The integrity string representing the expected content in the response. + * @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior. + */ +export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; + export interface ResourceRequest { name: string, // the name of the asset, including extension. behavior: AssetBehaviours, // determines how the asset will be handled once loaded @@ -129,24 +183,81 @@ export interface AssetEntry extends ResourceRequest { } export type AssetBehaviours = - "resource" // load asset as a managed resource assembly - | "assembly" // load asset as a managed assembly - | "pdb" // load asset as a managed debugging information - | "heap" // store asset into the native heap - | "icu" // load asset as an ICU data archive - | "vfs" // load asset into the virtual filesystem (for fopen, File.Open, etc) - | "dotnetwasm" // the binary of the dotnet runtime - | "js-module-threads" // the javascript module for threads - | "js-module-runtime" // the javascript module for threads - | "js-module-dotnet" // the javascript module for threads - | "js-module-native" // the javascript module for threads - | "symbols" // the javascript module for threads + /** + * Load asset as a managed resource assembly. + */ + "resource" + /** + * Load asset as a managed assembly. + */ + | "assembly" + /** + * Load asset as a managed debugging information. + */ + | "pdb" + /** + * Store asset into the native heap. + */ + | "heap" + /** + * Load asset as an ICU data archive. + */ + | "icu" + /** + * Load asset into the virtual filesystem (for fopen, File.Open, etc). + */ + | "vfs" + /** + * The binary of the dotnet runtime. + */ + | "dotnetwasm" + /** + * The javascript module for threads. + */ + | "js-module-threads" + /** + * The javascript module for threads. + */ + | "js-module-runtime" + /** + * The javascript module for threads. + */ + | "js-module-dotnet" + /** + * The javascript module for threads. + */ + | "js-module-native" + /** + * The javascript module that came from nuget package . + */ + | "js-module-library-initializer" + /** + * The javascript module for threads. + */ + | "symbols" // -export type GlobalizationMode = - "icu" | // load ICU globalization data from any runtime assets with behavior "icu". - "invariant" | // operate in invariant globalization mode. - "hybrid" | // operate in hybrid globalization mode with small ICU files, using native platform functions - "auto" // (default): if "icu" behavior assets are present, use ICU, otherwise invariant. +export const enum GlobalizationMode { + /** + * Load sharded ICU data. + */ + Sharded = "sharded", // + /** + * Load all ICU data. + */ + All = "all", + /** + * Operate in invariant globalization mode. + */ + Invariant = "invariant", + /** + * Use user defined icu file. + */ + Custom = "custom", + /** + * Operate in hybrid globalization mode with small ICU files, using native platform functions. + */ + Hybrid = "hybrid" +} export type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean, @@ -170,6 +281,7 @@ export type APIType = { getAssemblyExports(assemblyName: string): Promise, setModuleImports(moduleName: string, moduleImports: any): void, getConfig: () => MonoConfig, + invokeLibraryInitializers: (functionName: string, args: any[]) => Promise, // memory management setHeapB32: (offset: NativePointer, value: number | boolean) => void, diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 37f77e9f71388..d088b3de5368e 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { AssetBehaviours, AssetEntry, DotnetModuleConfig, LoadingResource, MonoConfig, ResourceRequest, RuntimeAPI, WebAssemblyStartOptions } from "."; +import type { AssetBehaviours, AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, ResourceRequest, RuntimeAPI } from "."; +import type { BootJsonData } from "./blazor"; import type { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr, Int32Ptr } from "./emscripten"; export type GCHandle = { @@ -75,8 +76,7 @@ export type MonoConfigInternal = MonoConfig & { logExitCode?: boolean forwardConsoleLogsToWS?: boolean, asyncFlushOnExit?: boolean - exitAfterSnapshot?: number, - startupOptions?: Partial, + exitAfterSnapshot?: number loadAllSatelliteResources?: boolean }; @@ -102,6 +102,7 @@ export type LoaderHelpers = { loadedFiles: string[], _loaded_files: { url: string, file: string }[]; + loadedAssemblies: string[], scriptDirectory: string scriptUrl: string modulesUniqueQuery?: string @@ -133,8 +134,14 @@ export type LoaderHelpers = { err(message: string): void; getApplicationEnvironment?: (bootConfigResponse: Response) => string | null; + hasDebuggingEnabled(bootConfig: BootJsonData): boolean, + + loadBootResource?: LoadBootResourceCallback; + invokeLibraryInitializers: (functionName: string, args: any[]) => Promise, + libraryInitializers?: { scriptName: string, exports: any }[]; + isChromium: boolean, - isFirefox: boolean, + isFirefox: boolean } export type RuntimeHelpers = { config: MonoConfigInternal; @@ -319,6 +326,12 @@ export interface JavaScriptExports { // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) get_managed_stack_trace(exception_gc_handle: GCHandle): string | null + + // the marshaled signature is: void LoadSatelliteAssembly(byte[] dll) + load_satellite_assembly(dll: Uint8Array): void; + + // the marshaled signature is: void LoadLazyAssembly(byte[] dll, byte[] pdb) + load_lazy_assembly(dll: Uint8Array, pdb: Uint8Array | null): void; } export type MarshalerToJs = (arg: JSMarshalerArgument, element_type?: MarshalerType, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs, arg3_converter?: MarshalerToCs) => any; diff --git a/src/mono/wasm/sln/WasmBuild.sln b/src/mono/wasm/sln/WasmBuild.sln index 63ad36cf1624f..b2ce61ddc3513 100755 --- a/src/mono/wasm/sln/WasmBuild.sln +++ b/src/mono/wasm/sln/WasmBuild.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmSymbolicator", "..\symb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplyUpdateReferencedAssembly", "..\debugger\tests\ApplyUpdateReferencedAssembly\ApplyUpdateReferencedAssembly.csproj", "{75477B6F-DC8E-4002-88B8-017C992C568E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.WebAssembly.Pack.Tasks", "..\..\..\tasks\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj", "{5EEC2925-2021-4830-B7E9-72BB8B2C283D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,10 +59,6 @@ Global {F5AE2AF5-3C30-45E3-A0C6-D962C51FE5E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F5AE2AF5-3C30-45E3-A0C6-D962C51FE5E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5AE2AF5-3C30-45E3-A0C6-D962C51FE5E7}.Release|Any CPU.Build.0 = Release|Any CPU - {75477B6F-DC8E-4002-88B8-017C992C568E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {75477B6F-DC8E-4002-88B8-017C992C568E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {75477B6F-DC8E-4002-88B8-017C992C568E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {75477B6F-DC8E-4002-88B8-017C992C568E}.Release|Any CPU.Build.0 = Release|Any CPU {C7099764-EC2E-4FAF-9057-0321893DE4F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7099764-EC2E-4FAF-9057-0321893DE4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7099764-EC2E-4FAF-9057-0321893DE4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -69,6 +67,14 @@ Global {ABC41254-EC2E-4FAF-9057-091ABF4DE4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABC41254-EC2E-4FAF-9057-091ABF4DE4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABC41254-EC2E-4FAF-9057-091ABF4DE4F8}.Release|Any CPU.Build.0 = Release|Any CPU + {75477B6F-DC8E-4002-88B8-017C992C568E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75477B6F-DC8E-4002-88B8-017C992C568E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75477B6F-DC8E-4002-88B8-017C992C568E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75477B6F-DC8E-4002-88B8-017C992C568E}.Release|Any CPU.Build.0 = Release|Any CPU + {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/AppSettingsTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/AppSettingsTest.cs new file mode 100644 index 0000000000000..2e09f35eb0029 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/AppSettingsTest.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text.Json; +using System.Runtime.InteropServices.JavaScript; + +public partial class AppSettingsTest +{ + [JSExport] + public static void Run() + { + // Check file presence in VFS based on application environment + PrintFileExistence("/appsettings.json"); + PrintFileExistence("/appsettings.Development.json"); + PrintFileExistence("/appsettings.Production.json"); + } + + // Synchronize with AppSettingsTests + private static void PrintFileExistence(string path) => TestOutput.WriteLine($"'{path}' exists '{File.Exists(path)}'"); +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/Common/Program.cs b/src/mono/wasm/testassets/WasmBasicTestApp/Common/Program.cs new file mode 100644 index 0000000000000..4f38c02031c7e --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/Common/Program.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +System.Console.WriteLine("WasmBasicTestApp"); diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/Common/TestOutput.cs b/src/mono/wasm/testassets/WasmBasicTestApp/Common/TestOutput.cs new file mode 100644 index 0000000000000..5755b753b5e49 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/Common/TestOutput.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +public static class TestOutput +{ + public static void WriteLine(string message) + { + Console.WriteLine("TestOutput -> " + message); + } + + public static void WriteLine(object message) + { + Console.Write("TestOutput -> "); + Console.WriteLine(message); + } +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/LazyLoadingTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/LazyLoadingTest.cs new file mode 100644 index 0000000000000..0d68b1216816c --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/LazyLoadingTest.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text.Json; +using System.Runtime.InteropServices.JavaScript; + +public partial class LazyLoadingTest +{ + [JSExport] + public static void Run() + { + // System.Text.Json is marked as lazy loaded in the csproj ("BlazorWebAssemblyLazyLoad"), this method can be called only after the assembly is lazy loaded + // In the test case it is done in the JS before call to this method + var text = JsonSerializer.Serialize(new Person("John", "Doe")); + TestOutput.WriteLine(text); + } + + public record Person(string FirstName, string LastName); +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/LibraryInitializerTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/LibraryInitializerTest.cs new file mode 100644 index 0000000000000..0b9b3f9d5ef8a --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/LibraryInitializerTest.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Threading.Tasks; +using System.Resources; +using System.Runtime.InteropServices.JavaScript; + +public partial class LibraryInitializerTest +{ + [JSExport] + public static void Run() + { + TestOutput.WriteLine($"LIBRARY_INITIALIZER_TEST = {Environment.GetEnvironmentVariable("LIBRARY_INITIALIZER_TEST")}"); + } +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/Properties/AssemblyInfo.cs b/src/mono/wasm/testassets/WasmBasicTestApp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000..9ad9b578f2064 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +[assembly:System.Runtime.Versioning.SupportedOSPlatform("browser")] diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/SatelliteAssembliesTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/SatelliteAssembliesTest.cs new file mode 100644 index 0000000000000..b8dc5b0dbf884 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/SatelliteAssembliesTest.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Threading.Tasks; +using System.Resources; +using System.Runtime.InteropServices.JavaScript; + +public partial class SatelliteAssembliesTest +{ + [JSExport] + public static async Task Run() + { + var rm = new ResourceManager("WasmBasicTestApp.words", typeof(Program).Assembly); + TestOutput.WriteLine("default: " + rm.GetString("hello", CultureInfo.CurrentCulture)); + TestOutput.WriteLine("es-ES without satellite: " + rm.GetString("hello", new CultureInfo("es-ES"))); + + await LoadSatelliteAssemblies(new[] { "es-ES" }); + + rm = new ResourceManager("WasmBasicTestApp.words", typeof(Program).Assembly); + TestOutput.WriteLine("default: " + rm.GetString("hello", CultureInfo.CurrentCulture)); + TestOutput.WriteLine("es-ES with satellite: " + rm.GetString("hello", new CultureInfo("es-ES"))); + } + + [JSImport("INTERNAL.loadSatelliteAssemblies")] + public static partial Task LoadSatelliteAssemblies(string[] culturesToLoad); +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/WasmBasicTestApp.csproj b/src/mono/wasm/testassets/WasmBasicTestApp/WasmBasicTestApp.csproj new file mode 100644 index 0000000000000..fb563ea6b52de --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/WasmBasicTestApp.csproj @@ -0,0 +1,12 @@ + + + net8.0 + browser-wasm + Exe + true + + + + + + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/words.es-ES.resx b/src/mono/wasm/testassets/WasmBasicTestApp/words.es-ES.resx new file mode 100644 index 0000000000000..775397b15a2b9 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/words.es-ES.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ciao + + + + hola + + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/words.resx b/src/mono/wasm/testassets/WasmBasicTestApp/words.resx new file mode 100644 index 0000000000000..c3d5a78742086 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/words.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + bye + + + + hello + + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/WasmBasicTestApp.lib.module.js b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/WasmBasicTestApp.lib.module.js new file mode 100644 index 0000000000000..200f17f6abaeb --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/WasmBasicTestApp.lib.module.js @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +const params = new URLSearchParams(location.search); + +export function onRuntimeConfigLoaded(config) { + config.environmentVariables["LIBRARY_INITIALIZER_TEST"] = "1"; + + if (params.get("throwError") === "true") { + throw new Error("Error thrown from library initializer"); + } +} + +export async function onRuntimeReady({ getAssemblyExports, getConfig }) { + const testCase = params.get("test"); + if (testCase == "LibraryInitializerTest") { + const config = getConfig(); + const exports = await getAssemblyExports(config.mainAssemblyName); + + exports.LibraryInitializerTest.Run(); + } +} \ No newline at end of file diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.Development.json b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.Development.json new file mode 100644 index 0000000000000..3a9ec3802e537 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.Development.json @@ -0,0 +1,4 @@ +{ + "key2": "Development key2-value", + "key3": "Development key3-value" +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.Production.json b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.Production.json new file mode 100644 index 0000000000000..539fe7d8318e9 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.Production.json @@ -0,0 +1,4 @@ +{ + "key2": "Prod key2-value", + "key3": "Prod key3-value" +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.json b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.json new file mode 100644 index 0000000000000..6534159aa03af --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/appsettings.json @@ -0,0 +1,12 @@ +{ + "key1": "Default key1-value", + "key2": "Default key2-value", + "Logging": { + "PrependMessage": { + "Message": "Custom logger", + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/index.html b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/index.html new file mode 100644 index 0000000000000..997c0d3047c08 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/index.html @@ -0,0 +1,17 @@ + + + + + + + WasmLazyLoading + + + + + + + + + + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/main.js new file mode 100644 index 0000000000000..127d506e3c9a6 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/main.js @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnet, exit } from './_framework/dotnet.js' + +// Read test case from query string +const params = new URLSearchParams(location.search); +const testCase = params.get("test"); +if (testCase == null) { + exit(2, new Error("Missing test scenario. Supply query argument 'test'.")); +} + +// Prepare base runtime parameters +dotnet + .withElementOnExit() + .withExitCodeLogging() + .withExitOnUnhandledError(); + +// Modify runtime start based on test case +switch (testCase) { + case "AppSettingsTest": + dotnet.withApplicationEnvironment(params.get("applicationEnvironment")); + break; +} + +const { getAssemblyExports, getConfig, INTERNAL } = await dotnet.create(); +const config = getConfig(); +const exports = await getAssemblyExports(config.mainAssemblyName); + +// Run the test case +try { + switch (testCase) { + case "SatelliteAssembliesTest": + await exports.SatelliteAssembliesTest.Run(); + exit(0); + break; + case "LazyLoadingTest": + if (params.get("loadRequiredAssembly") !== "false") { + await INTERNAL.loadLazyAssembly("System.Text.Json.wasm"); + } + exports.LazyLoadingTest.Run(); + exit(0); + break; + case "LibraryInitializerTest": + exit(0); + break; + case "AppSettingsTest": + exports.AppSettingsTest.Run(); + exit(0); + break; + } +} catch (e) { + exit(1, e); +} diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs index bf4beb053507f..81943d6ce83b0 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -129,10 +129,18 @@ public class ResourcesData /// /// JavaScript module initializers that Blazor will be in charge of loading. + /// Used in .NET < 8 /// [DataMember(EmitDefaultValue = false)] public ResourceHashesByNameDictionary libraryInitializers { get; set; } + /// + /// JavaScript module initializers that runtime will be in charge of loading. + /// Used in .NET >= 8 + /// + [DataMember(EmitDefaultValue = false)] + public TypedLibraryStartupModules libraryStartupModules { get; set; } + /// /// Extensions created by users customizing the initialization process. The format of the file(s) /// is up to the user. @@ -151,7 +159,16 @@ public class ResourcesData [DataMember(EmitDefaultValue = false)] public List remoteSources { get; set; } +} +[DataContract] +public class TypedLibraryStartupModules +{ + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary onRuntimeConfigLoaded { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary onRuntimeReady { get; set; } } public enum ICUDataMode : int diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs index 9690097816258..a423251b4cb58 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -10,7 +10,6 @@ using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text; -using System.Xml; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary; @@ -30,6 +29,8 @@ public class GenerateWasmBootJson : Task [Required] public bool DebugBuild { get; set; } + public string DebugLevel { get; set; } + [Required] public bool LinkerEnabled { get; set; } @@ -54,6 +55,13 @@ public class GenerateWasmBootJson : Task public string RuntimeOptions { get; set; } + [Required] + public string TargetFrameworkVersion { get; set; } + + public ITaskItem[] LibraryInitializerOnRuntimeConfigLoaded { get; set; } + + public ITaskItem[] LibraryInitializerOnRuntimeReady { get; set; } + [Required] public string OutputPath { get; set; } @@ -79,35 +87,16 @@ public override bool Execute() // Internal for tests public void WriteBootJson(Stream output, string entryAssemblyName) { - var icuDataMode = ICUDataMode.Sharded; - - if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase)) - { - icuDataMode = ICUDataMode.Invariant; - } - else if (IsHybridGlobalization) - { - icuDataMode = ICUDataMode.Hybrid; - } - else if (LoadAllICUData) - { - icuDataMode = ICUDataMode.All; - } - else if (LoadCustomIcuData) - { - icuDataMode = ICUDataMode.Custom; - } - var result = new BootJsonData { entryAssembly = entryAssemblyName, cacheBootResources = CacheBootResources, debugBuild = DebugBuild, - debugLevel = DebugBuild ? 1 : 0, + debugLevel = ParseOptionalInt(DebugLevel) ?? (DebugBuild ? 1 : 0), linkerEnabled = LinkerEnabled, resources = new ResourcesData(), config = new List(), - icuDataMode = icuDataMode, + icuDataMode = GetIcuDataMode(), startupMemoryCache = ParseOptionalBool(StartupMemoryCache), }; @@ -138,6 +127,9 @@ public void WriteBootJson(Stream output, string entryAssemblyName) result.runtimeOptions = runtimeOptions.ToArray(); } + string[] libraryInitializerOnRuntimeConfigLoadedFullPaths = LibraryInitializerOnRuntimeConfigLoaded?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); + string[] libraryInitializerOnRuntimeReadyFullPath = LibraryInitializerOnRuntimeReady?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); + // Build a two-level dictionary of the form: // - assembly: // - UriPath (e.g., "System.Text.Json.dll") @@ -211,14 +203,45 @@ public void WriteBootJson(Stream output, string entryAssemblyName) resourceList = resourceData.runtime; } else if (string.Equals("JSModule", assetTraitName, StringComparison.OrdinalIgnoreCase) && - string.Equals(assetTraitValue, "JSLibraryModule", StringComparison.OrdinalIgnoreCase)) + string.Equals(assetTraitValue, "JSLibraryModule", StringComparison.OrdinalIgnoreCase)) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a library initializer resource.", resource.ItemSpec); - resourceData.libraryInitializers ??= new(); - resourceList = resourceData.libraryInitializers; + var targetPath = resource.GetMetadata("TargetPath"); Debug.Assert(!string.IsNullOrEmpty(targetPath), "Target path for '{0}' must exist.", resource.ItemSpec); + + resourceList = resourceData.libraryInitializers ??= new ResourceHashesByNameDictionary(); AddResourceToList(resource, resourceList, targetPath); + + if (IsTargeting80OrLater()) + { + var libraryStartupModules = resourceData.libraryStartupModules ??= new TypedLibraryStartupModules(); + + if (libraryInitializerOnRuntimeConfigLoadedFullPaths.Contains(resource.ItemSpec)) + { + resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); + } + else if (libraryInitializerOnRuntimeReadyFullPath.Contains(resource.ItemSpec)) + { + resourceList = libraryStartupModules.onRuntimeReady ??= new(); + } + else if (File.Exists(resource.ItemSpec)) + { + string fileContent = File.ReadAllText(resource.ItemSpec); + if (fileContent.Contains("onRuntimeConfigLoaded") || fileContent.Contains("beforeStart") || fileContent.Contains("afterStarted")) + resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); + else + resourceList = libraryStartupModules.onRuntimeReady ??= new(); + } + else + { + resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); + } + + string newTargetPath = "../" + targetPath; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK + AddResourceToList(resource, resourceList, newTargetPath); + } + continue; } else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase) && @@ -281,7 +304,11 @@ public void WriteBootJson(Stream output, string entryAssemblyName) { foreach (var configFile in ConfigurationFiles) { - result.config.Add(Path.GetFileName(configFile.ItemSpec)); + string configUrl = Path.GetFileName(configFile.ItemSpec); + if (IsTargeting80OrLater()) + configUrl = "../" + configUrl; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK + + result.config.Add(configUrl); } } @@ -304,7 +331,9 @@ public void WriteBootJson(Stream output, string entryAssemblyName) var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings { - UseSimpleDictionaryFormat = true + UseSimpleDictionaryFormat = true, + KnownTypes = new[] { typeof(TypedLibraryStartupModules) }, + EmitTypeInformation = EmitTypeInformation.Never }); using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true); @@ -320,6 +349,20 @@ void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resour } } + private ICUDataMode GetIcuDataMode() + { + if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase)) + return ICUDataMode.Invariant; + else if (IsHybridGlobalization) + return ICUDataMode.Hybrid; + else if (LoadAllICUData) + return ICUDataMode.All; + else if (LoadCustomIcuData) + return ICUDataMode.Custom; + + return ICUDataMode.Sharded; + } + private static bool? ParseOptionalBool(string value) { if (string.IsNullOrEmpty(value) || !bool.TryParse(value, out var boolValue)) @@ -328,6 +371,14 @@ void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resour return boolValue; } + private static int? ParseOptionalInt(string value) + { + if (string.IsNullOrEmpty(value) || !int.TryParse(value, out var intValue)) + return null; + + return intValue; + } + private void AddToAdditionalResources(ITaskItem resource, Dictionary additionalResources, string resourceName, string behavior) { if (!additionalResources.ContainsKey(resourceName)) @@ -345,4 +396,21 @@ private bool TryGetLazyLoadedAssembly(string fileName, out ITaskItem lazyLoadedA { return (lazyLoadedAssembly = LazyLoadedAssemblies?.SingleOrDefault(a => a.ItemSpec == fileName)) != null; } + + private Version? parsedTargetFrameworkVersion; + private static readonly Version version80 = new Version(8, 0); + + private bool IsTargeting80OrLater() + { + if (parsedTargetFrameworkVersion == null) + { + string tfv = TargetFrameworkVersion; + if (tfv.StartsWith("v")) + tfv = tfv.Substring(1); + + parsedTargetFrameworkVersion = Version.Parse(tfv); + } + + return parsedTargetFrameworkVersion >= version80; + } }