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;
+ }
}