[browser] Unify boot config schema and output layout (#86255)
authorMarek Fišera <mara@neptuo.com>
Wed, 28 Jun 2023 03:57:02 +0000 (05:57 +0200)
committerGitHub <noreply@github.com>
Wed, 28 Jun 2023 03:57:02 +0000 (23:57 -0400)
- Update `WasmAppBuilder` to produce the same boot config schema as `GenerateWasmBootJson`
- Update AppBundle layout to match blazor layout.
    - All the runtime files (js, wasm, icu) are deployed to `_framework` folder.
    - This behavior can be overriden by setting `<WasmRuntimeAssetsLocation>` property. The empty value is overriden to default `_framework`, but `./` can be used to flatten the output structure.
    - WBT test with flat output
    - The only remaining difference is `AppBundle` vs `wwwrooot` folder name.
- `JSHost.ImportAsync` now requires the same relative paths as in blazor (`./module.js` => `../module.js`)

Contributes to https://github.com/dotnet/runtime/issues/70762
---------

Co-authored-by: Ankit Jain <radical@gmail.com>
49 files changed:
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.Legacy.UnitTests/System/Runtime/InteropServices/JavaScript/TimerTests.cs
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.cs
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js
src/mono/sample/mbr/browser/main.js
src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj
src/mono/sample/wasm/browser-advanced/index.html
src/mono/sample/wasm/browser-advanced/main.js
src/mono/sample/wasm/browser-bench/appstart-frame.html
src/mono/sample/wasm/browser-bench/frame-main.js
src/mono/sample/wasm/browser-bench/main.js
src/mono/sample/wasm/browser-eventpipe/main.js
src/mono/sample/wasm/browser-profile/main.js
src/mono/sample/wasm/browser-threads-minimal/Program.cs
src/mono/sample/wasm/browser-threads-minimal/main.js
src/mono/sample/wasm/browser-threads/main.js
src/mono/sample/wasm/browser-webpack/Wasm.Browser.WebPack.Sample.csproj
src/mono/sample/wasm/browser/main.js
src/mono/sample/wasm/console-node/main.mjs
src/mono/sample/wasm/console-v8/main.mjs
src/mono/sample/wasm/node-webpack/Wasm.Node.WebPack.Sample.csproj
src/mono/sample/wasm/simple-raytracer/main.js
src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs
src/mono/wasm/Wasm.Build.Tests/HostRunner/V8HostRunner.cs
src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs
src/mono/wasm/Wasm.Build.Tests/WasmTemplateTests.cs
src/mono/wasm/build/WasmApp.targets
src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs
src/mono/wasm/debugger/tests/debugger-test/debugger-main.js
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/loader/blazor/_Integration.ts
src/mono/wasm/runtime/loader/blazor/_Polyfill.ts
src/mono/wasm/runtime/loader/config.ts
src/mono/wasm/runtime/loader/polyfills.ts
src/mono/wasm/runtime/loader/run.ts
src/mono/wasm/runtime/pthreads/browser/index.ts
src/mono/wasm/runtime/types/blazor.ts
src/mono/wasm/runtime/types/internal.ts
src/mono/wasm/templates/templates/browser/main.js
src/mono/wasm/templates/templates/console/main.mjs
src/mono/wasm/test-main.js
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
src/tasks/WasmAppBuilder/WasmAppBuilder.cs
src/tasks/WasmAppBuilder/WasmAppBuilder.csproj
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/main.js
src/tests/FunctionalTests/WebAssembly/Browser/RuntimeConfig/main.js
src/tests/FunctionalTests/WebAssembly/Browser/StartupHook/main.js

index a66720c..5f16d50 100644 (file)
@@ -22,8 +22,8 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
         [Fact]
         public async Task MultipleImportAsync()
         {
-            var first = await JSHost.ImportAsync("JavaScriptTestHelper", "./JavaScriptTestHelper.mjs");
-            var second = await JSHost.ImportAsync("JavaScriptTestHelper", "./JavaScriptTestHelper.mjs");
+            var first = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs");
+            var second = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs");
             Assert.NotNull(first);
             Assert.NotNull(second);
             Assert.Equal("object", first.GetTypeOfProperty("instance"));
@@ -36,12 +36,12 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
         public async Task CancelableImportAsync()
         {
             var cts = new CancellationTokenSource();
-            var exTask = Assert.ThrowsAsync<JSException>(async () => await JSHost.ImportAsync("JavaScriptTestHelper", "./JavaScriptTestHelper.mjs", cts.Token));
+            var exTask = Assert.ThrowsAsync<JSException>(async () => await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs", cts.Token));
             cts.Cancel();
             var actualEx2 = await exTask;
             Assert.Equal("OperationCanceledException", actualEx2.Message);
 
-            var actualEx = await Assert.ThrowsAsync<JSException>(async () => await JSHost.ImportAsync("JavaScriptTestHelper", "./JavaScriptTestHelper.mjs", new CancellationToken(true)));
+            var actualEx = await Assert.ThrowsAsync<JSException>(async () => await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs", new CancellationToken(true)));
             Assert.Equal("OperationCanceledException", actualEx.Message);
         }
 
@@ -300,7 +300,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
             yield return new object[] { new object[] { "JSData" } }; // special cased, so we call createData in the test itself
             yield return new object[] { new object[] { new byte[] { }, new int[] { }, new double[] { }, new string[] { }, new object[] { } } };
             yield return new object[] { new object[] { new byte[] { 1, 2, 3 }, new int[] { 1, 2, 3 }, new double[] { 1, 2, 3 }, new string[] { "a", "b", "c" }, new object[] { } } };
-            yield return new object[] { new object[] { new object[] { new byte[] { 1, 2, 3 }, new int[] { 1, 2, 3 }, new double[] { 1, 2, 3 }, new string[] { "a", "b", "c" } , new object(), new SomethingRef(), new SomethingStruct(), new Exception("test") } } };
+            yield return new object[] { new object[] { new object[] { new byte[] { 1, 2, 3 }, new int[] { 1, 2, 3 }, new double[] { 1, 2, 3 }, new string[] { "a", "b", "c" }, new object(), new SomethingRef(), new SomethingStruct(), new Exception("test") } } };
             yield return new object[] { new object[] { } };
             yield return new object[] { null };
         }
@@ -317,10 +317,10 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
             Assert.Equal(expected, actual);
 
             if (expected != null) for (int i = 0; i < expected.Length; i++)
-            {
-                var actualI = JavaScriptTestHelper.store_ObjectArray(expected, i);
-                Assert.Equal(expected[i], actualI);
-            }
+                {
+                    var actualI = JavaScriptTestHelper.store_ObjectArray(expected, i);
+                    Assert.Equal(expected[i], actualI);
+                }
         }
 
         public static IEnumerable<object[]> MarshalObjectArrayCasesToDouble()
@@ -362,7 +362,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
             yield return new object[] { new object[] { (ushort)0 } };
             yield return new object[] { new object[] { new SomethingStruct[] { } } };
             yield return new object[] { new object[] { new SomethingRef[] { }, } };
-            yield return new object[] { new object[] { new ArraySegment<byte>(new byte[] { 11 }) , } };
+            yield return new object[] { new object[] { new ArraySegment<byte>(new byte[] { 11 }), } };
         }
         delegate void dummyDelegate();
         static void dummyDelegateA()
index 2a06708..fea5cc4 100644 (file)
@@ -68,7 +68,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
         [JSExport]
         public static void Optimized1V(int a1)
         {
-            optimizedReached+= a1;
+            optimizedReached += a1;
         }
         [JSImport("invoke1V", "JavaScriptTestHelper")]
         public static partial void invoke1V(int a1);
@@ -85,8 +85,8 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
         [JSExport]
         public static int Optimized2R(int a1, int a2)
         {
-            optimizedReached += a1+ a2;
-            return a1 + a2 +1;
+            optimizedReached += a1 + a2;
+            return a1 + a2 + 1;
         }
         [JSImport("invoke2R", "JavaScriptTestHelper")]
         public static partial int invoke2R(int a1, int a2);
@@ -997,7 +997,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
             if (_module == null)
             {
                 // Log("JavaScriptTestHelper.mjs importing");
-                _module = await JSHost.ImportAsync("JavaScriptTestHelper", "./JavaScriptTestHelper.mjs");
+                _module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs");
                 await Setup();
                 // Log("JavaScriptTestHelper.mjs imported");
             }
@@ -1010,7 +1010,7 @@ namespace JavaScriptTestHelperNamespace
     public partial class JavaScriptTestHelper
     {
         [System.Runtime.InteropServices.JavaScript.JSExport]
-        public static string EchoString(string message) 
+        public static string EchoString(string message)
         {
             return message + "11";
         }
@@ -1019,7 +1019,7 @@ namespace JavaScriptTestHelperNamespace
         {
             [System.Runtime.InteropServices.JavaScript.JSExport]
             public static string EchoString(string message) => message + "12";
-        
+
             private partial class DoubleNestedClass
             {
                 [System.Runtime.InteropServices.JavaScript.JSExport]
index 6b65ff6..7985ec9 100644 (file)
@@ -12,9 +12,9 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
     public partial class SecondRuntimeTest
     {
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowserDomSupportedOrNodeJS))]
-        public static async Task RunSecondRuntimeAndTestStaticState() 
+        public static async Task RunSecondRuntimeAndTestStaticState()
         {
-            await JSHost.ImportAsync("SecondRuntimeTest", "./SecondRuntimeTest.js");
+            await JSHost.ImportAsync("SecondRuntimeTest", "../SecondRuntimeTest.js");
 
             Interop.State = 42;
             var state2 = await Interop.RunSecondRuntimeAndTestStaticState();
index ff6a9d0..a3ffba9 100644 (file)
@@ -1,4 +1,4 @@
-import { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 try {
     const { getAssemblyExports } = await dotnet
@@ -8,7 +8,7 @@ try {
     const exports = await getAssemblyExports("WasmDelta.dll");
     const update = exports.Sample.Test.Update;
     const testMeaning = exports.Sample.Test.TestMeaning;
-    
+
     const outElement = document.getElementById("out");
     document.getElementById("update").addEventListener("click", function () {
         update();
index d37b174..6875be3 100644 (file)
@@ -14,6 +14,9 @@
     <_ServeHeaders>$(_ServeHeaders) -h &quot;Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval'&quot;</_ServeHeaders>
     <!-- enable reporting to profiler in browser dev tools -->
     <WasmProfilers>browser;</WasmProfilers>
+
+    <!-- Put "framework" (dotnet.js, dlls, etc) files directly into the AppBundle -->
+    <WasmRuntimeAssetsLocation>./</WasmRuntimeAssetsLocation>
   </PropertyGroup>
   <ItemGroup>
     <WasmExtraFilesToDeploy Include="main.js" />
index b0fd27b..eb1032f 100644 (file)
@@ -9,7 +9,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval';" />
   <script type='module' src="./main.js"></script>
-  <script type='module' src="./dotnet.js"></script>
+  <script type='module' src="./_framework/dotnet.js"></script>
   <link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
   <link rel="prefetch" href="./dotnet.native.js" as="fetch" crossorigin="anonymous">
   <link rel="prefetch" href="./dotnet.runtime.js" as="fetch" crossorigin="anonymous">
index 82be8cb..38a7236 100644 (file)
@@ -31,7 +31,7 @@ try {
         // here we show how emscripten could be further configured
         // It is preferred to use specific 'with***' methods instead in all other cases.
         .withModuleConfig({
-            configSrc: "./_framework/blazor.boot.json",
+            configSrc: "./blazor.boot.json",
             onConfigLoaded: (config) => {
                 // This is called during emscripten `dotnet.wasm` instantiation, after we fetched config.
                 console.log('user code Module.onConfigLoaded');
index 684acbc..697f7ac 100644 (file)
@@ -8,7 +8,7 @@
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <script type="module" src="./frame-main.js"></script>
-  <script type='module' src="./dotnet.js"></script>
+  <script type='module' src="./_framework/dotnet.js"></script>
   <link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
   <link rel="prefetch" href="./dotnet.native.js" as="fetch" crossorigin="anonymous">
   <link rel="prefetch" href="./dotnet.runtime.js" as="fetch" crossorigin="anonymous">
index 667f5ee..c104292 100644 (file)
@@ -3,7 +3,7 @@
 
 "use strict";
 
-import { dotnet, exit } from './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 class FrameApp {
     async init({ getAssemblyExports }) {
index 28d9613..eb9080e 100644 (file)
@@ -3,7 +3,7 @@
 
 "use strict";
 
-import { dotnet, exit } from './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 let runBenchmark;
 let setTasks;
index 683c076..e4f4c94 100644 (file)
@@ -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 { dotnet, exit } from "./dotnet.js";
+import { dotnet, exit } from "./_framework/dotnet.js";
 
 const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
 
index 455702e..aa31ff5 100644 (file)
@@ -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 { dotnet, exit } from './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 function saveProfile(aotProfileData) {
     if (!aotProfileData) {
index f86891e..c6a48fa 100644 (file)
@@ -99,7 +99,7 @@ namespace Sample
         [JSImport("globalThis.console.log")]
         private static partial void GlobalThisConsoleLog(string text);
 
-        const string fetchhelper = "./fetchelper.js";
+        const string fetchhelper = "../fetchhelper.js";
 
         [JSImport("responseText", fetchhelper)]
         private static partial Task<string> FetchHelperResponseText(JSObject response, int delayMs);
@@ -111,7 +111,7 @@ namespace Sample
         internal static Task TestHelloWebWorker()
         {
             Console.WriteLine($"smoke: TestHelloWebWorker 1 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
-            Task t= WebWorker.RunAsync(() => 
+            Task t = WebWorker.RunAsync(() =>
             {
                 Console.WriteLine($"smoke: TestHelloWebWorker 2 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
                 GlobalThisConsoleLog($"smoke: TestHelloWebWorker 3 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
@@ -121,7 +121,8 @@ namespace Sample
             return t.ContinueWith(Gogo);
         }
 
-        private static void Gogo(Task t){
+        private static void Gogo(Task t)
+        {
             Console.WriteLine($"smoke: TestHelloWebWorker 6 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
         }
 
@@ -153,9 +154,9 @@ namespace Sample
         internal static void StartTimerFromWorker()
         {
             Console.WriteLine("smoke: StartTimerFromWorker 1 utc {0}", DateTime.UtcNow.ToUniversalTime());
-            WebWorker.RunAsync(async () => 
+            WebWorker.RunAsync(async () =>
             {
-                while (!_timerDone)    
+                while (!_timerDone)
                 {
                     await Task.Delay(1 * 1000);
                     Console.WriteLine("smoke: StartTimerFromWorker 2 utc {0}", DateTime.UtcNow.ToUniversalTime());
@@ -168,19 +169,19 @@ namespace Sample
         internal static void StartAllocatorFromWorker()
         {
             Console.WriteLine("smoke: StartAllocatorFromWorker 1 utc {0}", DateTime.UtcNow.ToUniversalTime());
-            WebWorker.RunAsync(async () => 
+            WebWorker.RunAsync(async () =>
             {
-                while (!_timerDone)    
+                while (!_timerDone)
                 {
                     await Task.Delay(1 * 100);
                     var x = new List<int[]>();
                     for (int i = 0; i < 1000; i++)
                     {
-                        var v=new int[1000];
+                        var v = new int[1000];
                         v[i] = i;
                         x.Add(v);
                     }
-                    Console.WriteLine("smoke: StartAllocatorFromWorker 2 utc {0} {1} {2}", DateTime.UtcNow.ToUniversalTime(),x[1][1], GC.GetTotalAllocatedBytes());
+                    Console.WriteLine("smoke: StartAllocatorFromWorker 2 utc {0} {1} {2}", DateTime.UtcNow.ToUniversalTime(), x[1][1], GC.GetTotalAllocatedBytes());
                 }
                 Console.WriteLine("smoke: StartAllocatorFromWorker done utc {0}", DateTime.UtcNow.ToUniversalTime());
             });
@@ -196,7 +197,7 @@ namespace Sample
         public static async Task TestCallSetTimeoutOnWorker()
         {
             await WebWorker.RunAsync(() => TimeOutThenComplete());
-            Console.WriteLine ($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}");
+            Console.WriteLine($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}");
         }
 
         private static async Task<string> HttpClientGet(string name, string url)
@@ -274,7 +275,7 @@ namespace Sample
                 var ctx = SynchronizationContext.Current;
 
                 Console.WriteLine($"smoke: FetchBackground 2 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
-                var x=JSHost.ImportAsync(fetchhelper, "./fetchhelper.js");
+                var x = JSHost.ImportAsync(fetchhelper, "../fetchhelper.js");
                 Console.WriteLine($"smoke: FetchBackground 3A ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
                 // using var import = await x.ConfigureAwait(false);
                 Console.WriteLine($"smoke: FetchBackground 3B ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
@@ -285,11 +286,11 @@ namespace Sample
                 Console.WriteLine($"smoke: FetchBackground 5 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
                 if (ok)
                 {
-                    #if DEBUG
+#if DEBUG
                     var text = await FetchHelperResponseText(r, 5000);
-                    #else
+#else
                     var text = await FetchHelperResponseText(r, 25000);
-                    #endif
+#endif
                     Console.WriteLine($"smoke: FetchBackground 6 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
                     return text;
                 }
@@ -313,7 +314,7 @@ namespace Sample
             {
                 Console.WriteLine($"smoke {meaning}: TestTLS 2 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
                 meaning = 41;
-                await JSHost.ImportAsync(fetchhelper, "./fetchhelper.js");
+                await JSHost.ImportAsync(fetchhelper, "../fetchhelper.js");
                 Console.WriteLine($"smoke {meaning}: TestTLS 3 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
                 meaning = 43;
                 Console.WriteLine($"smoke {meaning}: TestTLS 4 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}");
@@ -327,21 +328,23 @@ namespace Sample
         private static async Task TimeOutThenComplete()
         {
             var tcs = new TaskCompletionSource();
-            Console.WriteLine ($"smoke: Task running tid:{Thread.CurrentThread.ManagedThreadId}");
-            GlobalThisSetTimeout(() => {
+            Console.WriteLine($"smoke: Task running tid:{Thread.CurrentThread.ManagedThreadId}");
+            GlobalThisSetTimeout(() =>
+            {
                 tcs.SetResult();
-                Console.WriteLine ($"smoke: Timeout fired tid:{Thread.CurrentThread.ManagedThreadId}");
+                Console.WriteLine($"smoke: Timeout fired tid:{Thread.CurrentThread.ManagedThreadId}");
             }, 250);
-            Console.WriteLine ($"smoke: Task sleeping tid:{Thread.CurrentThread.ManagedThreadId}");
+            Console.WriteLine($"smoke: Task sleeping tid:{Thread.CurrentThread.ManagedThreadId}");
             await tcs.Task;
-            Console.WriteLine ($"smoke: Task resumed tid:{Thread.CurrentThread.ManagedThreadId}");
+            Console.WriteLine($"smoke: Task resumed tid:{Thread.CurrentThread.ManagedThreadId}");
         }
 
         [JSExport]
         public static async Task<int> RunBackgroundThreadCompute()
         {
             var tcs = new TaskCompletionSource<int>();
-            var t = new Thread(() => {
+            var t = new Thread(() =>
+            {
                 var n = CountingCollatzTest();
                 tcs.SetResult(n);
             });
@@ -353,7 +356,8 @@ namespace Sample
         public static async Task<int> RunBackgroundLongRunningTaskCompute()
         {
             var factory = new TaskFactory();
-            var t = factory.StartNew<int> (() => {
+            var t = factory.StartNew<int>(() =>
+            {
                 var n = CountingCollatzTest();
                 return n;
             }, TaskCreationOptions.LongRunning);
@@ -363,17 +367,19 @@ namespace Sample
         [JSExport]
         public static async Task<int> RunBackgroundTaskRunCompute()
         {
-            var t1 = Task.Run (() => {
+            var t1 = Task.Run(() =>
+            {
                 var n = CountingCollatzTest();
                 return n;
             });
-            var t2 = Task.Run (() => {
+            var t2 = Task.Run(() =>
+            {
                 var n = CountingCollatzTest();
                 return n;
             });
-            var rs = await Task.WhenAll (new [] { t1, t2 });
+            var rs = await Task.WhenAll(new[] { t1, t2 });
             if (rs[0] != rs[1])
-                throw new Exception ($"Results from two tasks {rs[0]}, {rs[1]}, differ");
+                throw new Exception($"Results from two tasks {rs[0]}, {rs[1]}, differ");
             return rs[0];
         }
 
@@ -392,8 +398,9 @@ namespace Sample
             int bigly = 0;
             int hugely = 0;
             int maxSteps = 0;
-            for (int n = 1; n < maxInput; n++) {
-                int steps = CountingCollatz ((long)n, limit);
+            for (int n = 1; n < maxInput; n++)
+            {
+                int steps = CountingCollatz((long)n, limit);
                 if (steps > maxSteps)
                     maxSteps = steps;
                 if (steps > 120)
@@ -402,7 +409,7 @@ namespace Sample
                     hugely++;
             }
 
-            Console.WriteLine ($"Bigly: {bigly}, Hugely: {hugely}, maxSteps: {maxSteps}");
+            Console.WriteLine($"Bigly: {bigly}, Hugely: {hugely}, maxSteps: {maxSteps}");
 
             if (bigly == 86187 && hugely == 0 && maxSteps == 382)
                 return 524;
@@ -411,11 +418,12 @@ namespace Sample
         }
 
 
-        private static int CountingCollatz (long n, int limit)
+        private static int CountingCollatz(long n, int limit)
         {
             int steps = 0;
-            while (n > 1) {
-                n = Collatz1 (n);
+            while (n > 1)
+            {
+                n = Collatz1(n);
                 steps++;
                 if (steps >= limit)
                     break;
@@ -423,7 +431,7 @@ namespace Sample
             return steps;
         }
 
-        private static long Collatz1 (long n)
+        private static long Collatz1(long n)
         {
             if (n <= 0)
                 throw new Exception("Unexpected non-positive input");
index 59a0f4b..8ff5ab2 100644 (file)
@@ -1,13 +1,15 @@
 // 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 './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 const assemblyName = "Wasm.Browser.Threads.Minimal.Sample.dll";
 
 
 try {
-    const { setModuleImports, getAssemblyExports, runMain } = await dotnet
+    const resolveUrl = (relativeUrl) => (new URL(relativeUrl, window.location.href)).toString()
+
+    const { getAssemblyExports, runMain } = await dotnet
         //.withEnvironmentVariable("MONO_LOG_LEVEL", "debug")
         //.withDiagnosticTracing(true)
         .withConfig({
@@ -79,7 +81,7 @@ try {
 
     /* ActiveIssue https://github.com/dotnet/runtime/issues/88057
     console.log("smoke: running FetchBackground(blurst.txt)");
-    let s = await exports.Sample.Test.FetchBackground("./blurst.txt");
+    let s = await exports.Sample.Test.FetchBackground(resolveUrl("./blurst.txt"));
     console.log("smoke: FetchBackground(blurst.txt) done");
     if (!s.startsWith("It was the best of times, it was the blurst of times.")) {
         const msg = `Unexpected FetchBackground result ${s}`;
@@ -88,7 +90,7 @@ try {
     }
 
     console.log("smoke: running FetchBackground(missing)");
-    s = await exports.Sample.Test.FetchBackground("./missing.txt");
+    s = await exports.Sample.Test.FetchBackground(resolveUrl("./missing.txt"));
     console.log("smoke: FetchBackground(missing) done");
     if (s !== "not-ok") {
         const msg = `Unexpected FetchBackground(missing) result ${s}`;
index 9b739d4..c97e380 100644 (file)
@@ -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 { dotnet, exit } from './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 let progressElement = null;
 
index bb694f2..0459f51 100644 (file)
@@ -9,18 +9,18 @@
   </ItemGroup>
 
 
-  <Target Name="CopyRelinkedPackage" AfterTargets="WasmBuildApp" DependsOnTargets="Build" Inputs="$(WasmAppDir)/dotnet.js;
-          $(WasmAppDir)/dotnet.runtime.js;
-          $(WasmAppDir)/dotnet.native.js;
-          $(WasmAppDir)/dotnet.native.wasm;
+  <Target Name="CopyRelinkedPackage" AfterTargets="WasmBuildApp" DependsOnTargets="Build" Inputs="$(WasmAppDir)/_framework/dotnet.js;
+          $(WasmAppDir)/_framework/dotnet.runtime.js;
+          $(WasmAppDir)/_framework/dotnet.native.js;
+          $(WasmAppDir)/_framework/dotnet.native.wasm;
           $(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet.d.ts;
           $(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet-legacy.d.ts;
           $(MicrosoftNetCoreAppRuntimePackNativeDir)/package.json;" Outputs="bin/dotnet-runtime/.npm-stamp">
     <ItemGroup>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.js"/>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.runtime.js"/>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.native.js"/>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.native.wasm"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.js"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.runtime.js"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.native.js"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.native.wasm"/>
       <NpmPackageFiles Include="$(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet.d.ts"/>
       <NpmPackageFiles Include="$(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet-legacy.d.ts"/>
       <NpmPackageFiles Include="$(MicrosoftNetCoreAppRuntimePackNativeDir)/package.json"/>
index 2778308..ba84cca 100644 (file)
@@ -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 { dotnet, exit } from './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 function displayMeaning(meaning) {
     document.getElementById("out").innerHTML = `${meaning}`;
index 3b9741a..d3cfafe 100644 (file)
@@ -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 { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 await dotnet
     .withDiagnosticTracing(false)
index b336fe2..4350b81 100644 (file)
@@ -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 { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 await dotnet
     .withDiagnosticTracing(false)
index b10e782..88216d3 100644 (file)
@@ -4,18 +4,18 @@
     <WasmMainJSPath>main.mjs</WasmMainJSPath>
   </PropertyGroup>
 
-  <Target Name="CopyRelinkedPackage" AfterTargets="WasmBuildApp" DependsOnTargets="Build" Inputs="$(WasmAppDir)/dotnet.js;
-          $(WasmAppDir)/dotnet.runtime.js;
-          $(WasmAppDir)/dotnet.native.js;
-          $(WasmAppDir)/dotnet.native.wasm;
+  <Target Name="CopyRelinkedPackage" AfterTargets="WasmBuildApp" DependsOnTargets="Build" Inputs="$(WasmAppDir)/_framework/dotnet.js;
+          $(WasmAppDir)/_framework/dotnet.runtime.js;
+          $(WasmAppDir)/_framework/dotnet.native.js;
+          $(WasmAppDir)/_framework/dotnet.native.wasm;
           $(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet.d.ts;
           $(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet-legacy.d.ts;
           $(MicrosoftNetCoreAppRuntimePackNativeDir)/package.json;" Outputs="bin/dotnet-runtime/.npm-stamp">
     <ItemGroup>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.js"/>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.runtime.js"/>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.native.js"/>
-      <NpmPackageFiles Include="$(WasmAppDir)/dotnet.native.wasm"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.js"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.runtime.js"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.native.js"/>
+      <NpmPackageFiles Include="$(WasmAppDir)/_framework/dotnet.native.wasm"/>
       <NpmPackageFiles Include="$(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet.d.ts"/>
       <NpmPackageFiles Include="$(MicrosoftNetCoreAppRuntimePackNativeDir)/dotnet-legacy.d.ts"/>
       <NpmPackageFiles Include="$(MicrosoftNetCoreAppRuntimePackNativeDir)/package.json"/>
index b0f685a..decdf64 100644 (file)
@@ -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 { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 function renderCanvas(rgbaView) {
     const canvas = document.getElementById("out");
index 51cba2b..8e0f235 100644 (file)
@@ -386,9 +386,15 @@ namespace Wasm.Build.Tests
                 options.InitProject?.Invoke();
 
                 File.WriteAllText(Path.Combine(_projectDir, $"{buildArgs.ProjectName}.csproj"), buildArgs.ProjectFileContents);
-                File.Copy(Path.Combine(AppContext.BaseDirectory,
-                                        options.TargetFramework == "net8.0" ? "test-main.js" : "data/test-main-7.0.js"),
-                            Path.Combine(_projectDir, "test-main.js"));
+                File.Copy(
+                    Path.Combine(
+                        AppContext.BaseDirectory,
+                        string.IsNullOrEmpty(options.TargetFramework) || options.TargetFramework == "net8.0"
+                            ? "test-main.js"
+                            : "data/test-main-7.0.js"
+                    ),
+                    Path.Combine(_projectDir, "test-main.js")
+                );
 
                 File.WriteAllText(Path.Combine(_projectDir!, "index.html"), @"<html><body><script type=""module"" src=""test-main.js""></script></body></html>");
             }
@@ -676,11 +682,11 @@ namespace Wasm.Build.Tests
             var filesToExist = new List<string>()
             {
                 mainJS,
-                "dotnet.native.wasm",
+                "_framework/dotnet.native.wasm",
                 "_framework/blazor.boot.json",
-                "dotnet.js",
-                "dotnet.native.js",
-                "dotnet.runtime.js"
+                "_framework/dotnet.js",
+                "_framework/dotnet.native.js",
+                "_framework/dotnet.runtime.js"
             };
 
             if (isBrowserProject)
@@ -691,7 +697,7 @@ namespace Wasm.Build.Tests
             AssertFilesExist(bundleDir, new[] { "run-v8.sh" }, expectToExist: hasV8Script);
             AssertIcuAssets();
 
-            string managedDir = Path.Combine(bundleDir, "managed");
+            string managedDir = Path.Combine(bundleDir, "_framework");
             string bundledMainAppAssembly =
                 useWebcil ? $"{projectName}{WebcilInWasmExtension}" : $"{projectName}.dll";
             AssertFilesExist(managedDir, new[] { bundledMainAppAssembly });
@@ -732,7 +738,7 @@ namespace Wasm.Build.Tests
                     case GlobalizationMode.PredefinedIcu:
                         if (string.IsNullOrEmpty(predefinedIcudt))
                             throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu.");
-                        AssertFilesExist(bundleDir, new[] { predefinedIcudt }, expectToExist: true);
+                        AssertFilesExist(bundleDir, new[] { Path.Combine("_framework", predefinedIcudt) }, expectToExist: true);
                         // predefined ICU name can be identical with the icu files from runtime pack
                         switch (predefinedIcudt)
                         {
@@ -757,30 +763,32 @@ namespace Wasm.Build.Tests
                         expectNOCJK = true;
                         break;
                 }
-                AssertFilesExist(bundleDir, new[] { "icudt.dat" }, expectToExist: expectFULL);
-                AssertFilesExist(bundleDir, new[] { "icudt_EFIGS.dat" }, expectToExist: expectEFIGS);
-                AssertFilesExist(bundleDir, new[] { "icudt_CJK.dat" }, expectToExist: expectCJK);
-                AssertFilesExist(bundleDir, new[] { "icudt_no_CJK.dat" }, expectToExist: expectNOCJK);
-                AssertFilesExist(bundleDir, new[] { "icudt_hybrid.dat" }, expectToExist: expectHYBRID);
+
+                var frameworkDir = Path.Combine(bundleDir, "_framework");
+                AssertFilesExist(frameworkDir, new[] { "icudt.dat" }, expectToExist: expectFULL);
+                AssertFilesExist(frameworkDir, new[] { "icudt_EFIGS.dat" }, expectToExist: expectEFIGS);
+                AssertFilesExist(frameworkDir, new[] { "icudt_CJK.dat" }, expectToExist: expectCJK);
+                AssertFilesExist(frameworkDir, new[] { "icudt_no_CJK.dat" }, expectToExist: expectNOCJK);
+                AssertFilesExist(frameworkDir, new[] { "icudt_hybrid.dat" }, expectToExist: expectHYBRID);
             }
         }
 
         protected static void AssertDotNetWasmJs(string bundleDir, bool fromRuntimePack, string targetFramework)
         {
             AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.wasm"),
-                       Path.Combine(bundleDir, "dotnet.native.wasm"),
+                       Path.Combine(bundleDir, "_framework/dotnet.native.wasm"),
                        "Expected dotnet.native.wasm to be same as the runtime pack",
                        same: fromRuntimePack);
 
             AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js"),
-                       Path.Combine(bundleDir, "dotnet.native.js"),
+                       Path.Combine(bundleDir, "_framework/dotnet.native.js"),
                        "Expected dotnet.native.js to be same as the runtime pack",
                        same: fromRuntimePack);
         }
 
         protected static void AssertDotNetJsSymbols(string bundleDir, bool fromRuntimePack, string targetFramework)
             => AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js.symbols"),
-                            Path.Combine(bundleDir, "dotnet.native.js.symbols"),
+                            Path.Combine(bundleDir, "_framework/dotnet.native.js.symbols"),
                             same: fromRuntimePack);
 
         protected static void AssertFilesDontExist(string dir, string[] filenames, string? label = null)
index 76067e4..8a797ef 100644 (file)
@@ -9,7 +9,7 @@ using System.Runtime.InteropServices;
 
 public class V8HostRunner : IHostRunner
 {
-    private string GetXharnessArgs(string jsRelativePath) => $"--js-file={jsRelativePath} --engine=V8 -v trace --engine-arg=--experimental-wasm-simd";
+    private string GetXharnessArgs(string jsRelativePath) => $"--js-file={jsRelativePath} --engine=V8 -v trace --engine-arg=--experimental-wasm-simd --engine-arg=--module";
 
     public string GetTestCommand() => "wasm test";
     public string GetXharnessArgsWindowsOS(XHarnessArgsOptions options) => GetXharnessArgs(options.jsRelativePath);
index d31f990..d190dc2 100644 (file)
@@ -164,8 +164,8 @@ namespace Wasm.Build.NativeRebuild.Tests
                 Path.Combine(paths.ObjWasmDir, "pinvoke-table.h"),
                 Path.Combine(paths.ObjWasmDir, "driver-gen.c"),
 
-                Path.Combine(paths.BundleDir, "dotnet.native.wasm"),
-                Path.Combine(paths.BundleDir, "dotnet.native.js"),
+                Path.Combine(paths.BundleDir, "_framework", "dotnet.native.wasm"),
+                Path.Combine(paths.BundleDir, "_framework", "dotnet.native.js"),
             };
 
             if (buildArgs.AOT)
@@ -185,8 +185,8 @@ namespace Wasm.Build.NativeRebuild.Tests
                 dict[Path.GetFileName(file)] = (file, unchanged);
 
             // those files do not change on re-link
-            dict["dotnet.js"]=(Path.Combine(paths.BundleDir, "dotnet.js"), true);
-            dict["dotnet.runtime.js"]=(Path.Combine(paths.BundleDir, "dotnet.runtime.js"), true);
+            dict["dotnet.js"]=(Path.Combine(paths.BundleDir, "_framework", "dotnet.js"), true);
+            dict["dotnet.runtime.js"]=(Path.Combine(paths.BundleDir, "_framework", "dotnet.runtime.js"), true);
 
             return dict;
         }
index 8b58299..88af535 100644 (file)
@@ -36,7 +36,9 @@ namespace Wasm.Build.Tests
             File.WriteAllText(path, text);
         }
 
-        private void UpdateBrowserMainJs(string targetFramework)
+        private const string DefaultRuntimeAssetsRelativePath = "./_framework/";
+
+        private void UpdateBrowserMainJs(string targetFramework, string runtimeAssetsRelativePath = DefaultRuntimeAssetsRelativePath)
         {
             string mainJsPath = Path.Combine(_projectDir!, "main.js");
             string mainJsContent = File.ReadAllText(mainJsPath);
@@ -46,6 +48,9 @@ namespace Wasm.Build.Tests
                     targetFramework == "net8.0"
                         ? ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().withExitOnUnhandledError().create()"
                         : ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()");
+
+            mainJsContent = mainJsContent.Replace("from './_framework/dotnet.js'", $"from '{runtimeAssetsRelativePath}dotnet.js'");
+
             File.WriteAllText(mainJsPath, mainJsContent);
         }
 
@@ -423,21 +428,23 @@ namespace Wasm.Build.Tests
         }
 
         [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
-        [InlineData("", BuildTestBase.DefaultTargetFramework)]
+        [InlineData("", BuildTestBase.DefaultTargetFramework, DefaultRuntimeAssetsRelativePath)]
+        [InlineData("", BuildTestBase.DefaultTargetFramework, "./")]
         // [ActiveIssue("https://github.com/dotnet/runtime/issues/79313")]
         // [InlineData("-f net7.0", "net7.0")]
-        [InlineData("-f net8.0", "net8.0")]
-        public async Task BrowserBuildAndRun(string extraNewArgs, string targetFramework)
+        [InlineData("-f net8.0", "net8.0", DefaultRuntimeAssetsRelativePath)]
+        [InlineData("-f net8.0", "net8.0", "./")]
+        public async Task BrowserBuildAndRun(string extraNewArgs, string targetFramework, string runtimeAssetsRelativePath)
         {
             string config = "Debug";
             string id = $"browser_{config}_{Path.GetRandomFileName()}";
             CreateWasmTemplateProject(id, "wasmbrowser", extraNewArgs);
 
-            UpdateBrowserMainJs(targetFramework);
+            UpdateBrowserMainJs(targetFramework, runtimeAssetsRelativePath);
 
             new DotNetCommand(s_buildEnv, _testOutput)
                     .WithWorkingDirectory(_projectDir!)
-                    .Execute($"build -c {config} -bl:{Path.Combine(s_buildEnv.LogRootPath, $"{id}.binlog")}")
+                    .Execute($"build -c {config} -bl:{Path.Combine(s_buildEnv.LogRootPath, $"{id}.binlog")} {(runtimeAssetsRelativePath != DefaultRuntimeAssetsRelativePath ? "-p:WasmRuntimeAssetsLocation=" + runtimeAssetsRelativePath : "")}")
                     .EnsureSuccessful();
 
             using var runCommand = new RunCommand(s_buildEnv, _testOutput)
index 9290896..e76fd80 100644 (file)
       - $(WasmAllowUndefinedSymbols)        - Controls whether undefined symbols are allowed or not,
                                               if true, appends 'allow-undefined' and sets 'ERROR_ON_UNDEFINED_SYMBOLS=0' as arguments for wasm-ld,
                                               if false (default), removes 'allow-undefined' and sets 'ERROR_ON_UNDEFINED_SYMBOLS=1'.
+      - $(WasmRuntimeAssetsLocation)        - Allows to override a location for build generated files. 
+                                              Defaults to '_framework', if you want to put framework files in the same directory as user files, use './' value.
+                                              Output structure
+                                              - AppBundle directly contains user files
+                                              - AppBundle/_framework contains generated files (dlls, runtime scripts, icu)
+                                              - AppBundle/_content contains web files from nuget packages (css, js, etc)
 
       Public items:
       - @(WasmExtraFilesToDeploy) - Files to copy to $(WasmAppDir).
 
     <!-- by default, package assemblies as webcil -->
     <WasmEnableWebcil Condition="'$(WasmEnableWebcil)' == ''">true</WasmEnableWebcil>
+
+    <WasmRuntimeAssetsLocation Condition="'$(WasmRuntimeAssetsLocation)' == ''">_framework</WasmRuntimeAssetsLocation>
   </PropertyGroup>
 
   <ItemGroup>
       IncludeThreadsWorker="$(_WasmAppIncludeThreadsWorker)"
       PThreadPoolSize="$(_WasmPThreadPoolSize)"
       UseWebcil="$(WasmEnableWebcil)"
+      WasmIncludeFullIcuData="$(WasmIncludeFullIcuData)"
+      WasmIcuDataFileName="$(WasmIcuDataFileName)"
+      RuntimeAssetsLocation="$(WasmRuntimeAssetsLocation)"
       >
       <Output TaskParameter="FileWrites" ItemName="FileWrites" />
     </WasmAppBuilder>
index 46e054e..9e365c0 100644 (file)
@@ -94,8 +94,8 @@ namespace DebuggerTests
         public async Task DuplicateAssemblyLoadedEventForAssemblyFromBundle(bool load_pdb, int expected_count)
             => await AssemblyLoadedEventTest(
                 "debugger-test",
-                Path.Combine(DebuggerTestAppPath, "managed/debugger-test.dll"),
-                load_pdb ? Path.Combine(DebuggerTestAppPath, "managed/debugger-test.pdb") : null,
+                Path.Combine(DebuggerTestAppPath, "_framework/debugger-test.dll"),
+                load_pdb ? Path.Combine(DebuggerTestAppPath, "_framework/debugger-test.pdb") : null,
                 "/debugger-test.cs",
                 expected_count
             );
index 6f39603..cb6273e 100644 (file)
@@ -3,7 +3,7 @@
 
 "use strict";
 
-import { dotnet, exit } from './dotnet.js'
+import { dotnet, exit } from './_framework/dotnet.js'
 
 try {
     const runtime = await dotnet
index 7e055b7..50c6324 100644 (file)
@@ -315,6 +315,11 @@ interface BootJsonData {
     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;
 }
@@ -322,6 +327,7 @@ type BootJsonDataExtension = {
     [extensionName: string]: ResourceList;
 };
 interface ResourceGroups {
+    readonly hash?: string;
     readonly assembly: ResourceList;
     readonly lazyAssembly: ResourceList;
     readonly pdb?: ResourceList;
@@ -332,6 +338,9 @@ interface ResourceGroups {
     readonly libraryInitializers?: ResourceList;
     readonly extensions?: BootJsonDataExtension;
     readonly runtimeAssets: ExtendedResourceList;
+    readonly vfs?: {
+        [virtualPath: string]: ResourceList;
+    };
 }
 type ResourceList = {
     [name: string]: string;
index 61b2565..32ca65a 100644 (file)
@@ -5,7 +5,7 @@ import type { DotnetModuleInternal, MonoConfigInternal } from "../../types/inter
 import type { AssetBehaviours, AssetEntry, LoadingResource, WebAssemblyBootResourceType, WebAssemblyStartOptions } from "../../types";
 import type { BootJsonData } from "../../types/blazor";
 
-import { INTERNAL, loaderHelpers } from "../globals";
+import { ENVIRONMENT_IS_WEB, INTERNAL, loaderHelpers } from "../globals";
 import { BootConfigResult } from "./BootConfig";
 import { WebAssemblyResourceLoader } from "./WebAssemblyResourceLoader";
 import { hasDebuggingEnabled } from "./_Polyfill";
@@ -22,7 +22,10 @@ export async function loadBootConfig(config: MonoConfigInternal, module: DotnetM
 export async function initializeBootConfig(bootConfigResult: BootConfigResult, module: DotnetModuleInternal, startupOptions?: Partial<WebAssemblyStartOptions>) {
     INTERNAL.resourceLoader = resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, startupOptions ?? {});
     mapBootConfigToMonoConfig(loaderHelpers.config, bootConfigResult.applicationEnvironment);
-    setupModuleForBlazor(module);
+
+    if (ENVIRONMENT_IS_WEB) {
+        setupModuleForBlazor(module);
+    }
 }
 
 let resourcesLoaded = 0;
@@ -34,8 +37,9 @@ const behaviorByName = (name: string): AssetBehaviours | "other" => {
             : (name.startsWith("dotnet.native") && name.endsWith(".js")) ? "js-module-native"
                 : (name.startsWith("dotnet.runtime") && name.endsWith(".js")) ? "js-module-runtime"
                     : (name.startsWith("dotnet") && name.endsWith(".js")) ? "js-module-dotnet"
-                        : name.startsWith("icudt") ? "icu"
-                            : "other";
+                        : (name.startsWith("dotnet.native") && name.endsWith(".symbols")) ? "symbols"
+                            : name.startsWith("icudt") ? "icu"
+                                : "other";
 };
 
 const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = {
@@ -71,30 +75,52 @@ export function setupModuleForBlazor(module: DotnetModuleInternal) {
     };
 
     loaderHelpers.downloadResource = downloadResource; // polyfills were already assigned
-    module.disableDotnet6Compatibility = false;
+}
+
+function appendUniqueQuery(attemptUrl: string): string {
+    if (loaderHelpers.assetUniqueQuery) {
+        attemptUrl = attemptUrl + loaderHelpers.assetUniqueQuery;
+    }
+
+    return attemptUrl;
 }
 
 export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, applicationEnvironment: string) {
     const resources = resourceLoader.bootConfig.resources;
 
     const assets: AssetEntry[] = [];
-    const environmentVariables: any = {};
+    const environmentVariables: any = {
+        // From boot config
+        ...(resourceLoader.bootConfig.environmentVariables || {}),
+        // From JavaScript
+        ...(moduleConfig.environmentVariables || {})
+    };
 
     moduleConfig.applicationEnvironment = applicationEnvironment;
 
+    moduleConfig.remoteSources = (resourceLoader.bootConfig.resources as any).remoteSources;
+    moduleConfig.assetsHash = resourceLoader.bootConfig.resources.hash;
     moduleConfig.assets = assets;
     moduleConfig.globalizationMode = "icu";
-    moduleConfig.environmentVariables = environmentVariables;
-    moduleConfig.debugLevel = hasDebuggingEnabled(resourceLoader.bootConfig) ? 1 : 0;
-    moduleConfig.maxParallelDownloads = 1000000; // disable throttling parallel downloads
-    moduleConfig.enableDownloadRetry = false; // disable retry downloads
+    moduleConfig.debugLevel = hasDebuggingEnabled(resourceLoader.bootConfig) ? resourceLoader.bootConfig.debugLevel : 0;
     moduleConfig.mainAssemblyName = resourceLoader.bootConfig.entryAssembly;
 
+    const anyBootConfig = (resourceLoader.bootConfig as any);
+    for (const key in resourceLoader.bootConfig) {
+        if (Object.prototype.hasOwnProperty.call(anyBootConfig, key)) {
+            if (anyBootConfig[key] === null) {
+                delete anyBootConfig[key];
+            }
+        }
+    }
+
     // FIXME this mix of both formats is ugly temporary hack
     Object.assign(moduleConfig, {
         ...resourceLoader.bootConfig,
     });
 
+    moduleConfig.environmentVariables = environmentVariables;
+
     if (resourceLoader.bootConfig.startupMemoryCache !== undefined) {
         moduleConfig.startupMemoryCache = resourceLoader.bootConfig.startupMemoryCache;
     }
@@ -107,13 +133,13 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl
     for (const name in resources.runtimeAssets) {
         const asset = resources.runtimeAssets[name] as AssetEntry;
         asset.name = name;
-        asset.resolvedUrl = loaderHelpers.locateFile(name);
+        asset.resolvedUrl = appendUniqueQuery(loaderHelpers.locateFile(name));
         assets.push(asset);
     }
     for (const name in resources.assembly) {
         const asset: AssetEntry = {
             name,
-            resolvedUrl: loaderHelpers.locateFile(name),
+            resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name)),
             hash: resources.assembly[name],
             behavior: "assembly",
         };
@@ -123,18 +149,19 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl
         for (const name in resources.pdb) {
             const asset: AssetEntry = {
                 name,
-                resolvedUrl: loaderHelpers.locateFile(name),
+                resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name)),
                 hash: resources.pdb[name],
                 behavior: "pdb",
             };
             assets.push(asset);
         }
     }
-    const applicationCulture = resourceLoader.startOptions.applicationCulture || (navigator.languages && navigator.languages[0]);
-    const icuDataResourceName = getICUResourceName(resourceLoader.bootConfig, applicationCulture);
+    const applicationCulture = resourceLoader.startOptions.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) {
         const behavior = behaviorByName(name) as any;
+        let loadRemote = false;
         if (behavior === "icu") {
             if (resourceLoader.bootConfig.icuDataMode === ICUDataMode.Invariant) {
                 continue;
@@ -142,6 +169,7 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl
             if (name !== icuDataResourceName) {
                 continue;
             }
+            loadRemote = true;
             hasIcuData = true;
         } else if (behavior === "js-module-dotnet") {
             continue;
@@ -149,26 +177,54 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl
             continue;
         }
 
-        const resolvedUrl = loaderHelpers.locateFile(name);
+        const resolvedUrl = appendUniqueQuery(loaderHelpers.locateFile(name));
         const asset: AssetEntry = {
             name,
             resolvedUrl,
             hash: resources.runtime[name],
             behavior,
+            loadRemote
         };
         assets.push(asset);
     }
+
+    if (moduleConfig.loadAllSatelliteResources && resources.satelliteResources) {
+        for (const culture in resources.satelliteResources) {
+            for (const name in resources.satelliteResources[culture]) {
+                assets.push({
+                    name,
+                    culture,
+                    behavior: "resource",
+                    hash: resources.satelliteResources[culture][name],
+                });
+            }
+        }
+    }
+
     for (let i = 0; i < resourceLoader.bootConfig.config.length; i++) {
         const config = resourceLoader.bootConfig.config[i];
         if (config === "appsettings.json" || config === `appsettings.${applicationEnvironment}.json`) {
             assets.push({
                 name: config,
-                resolvedUrl: (document ? document.baseURI : "/") + config,
+                resolvedUrl: appendUniqueQuery((document ? document.baseURI : "/") + config),
                 behavior: "vfs",
             });
         }
     }
 
+    for (const virtualPath in resources.vfs) {
+        for (const name in resources.vfs[virtualPath]) {
+            const asset: AssetEntry = {
+                name,
+                resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name)),
+                hash: resources.vfs[virtualPath][name],
+                behavior: "vfs",
+                virtualPath
+            };
+            assets.push(asset);
+        }
+    }
+
     if (!hasIcuData) {
         moduleConfig.globalizationMode = "invariant";
     }
@@ -192,24 +248,26 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl
     }
 }
 
-function getICUResourceName(bootConfig: BootJsonData, culture: string | undefined): string {
+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";
             const customIcuFile = icuFiles[0];
             return customIcuFile;
         }
     }
 
-    if (bootConfig.icuDataMode === ICUDataMode.Hybrid)
-    {
+    if (bootConfig.icuDataMode === ICUDataMode.Hybrid) {
+        moduleConfig.globalizationMode = "hybrid";
         const reducedICUResourceName = "icudt_hybrid.dat";
         return reducedICUResourceName;
     }
 
     if (!culture || bootConfig.icuDataMode === ICUDataMode.All) {
+        moduleConfig.globalizationMode = "icu";
         const combinedICUResourceName = "icudt.dat";
         return combinedICUResourceName;
     }
index ea9b13b..efe700b 100644 (file)
@@ -13,9 +13,10 @@ export function toAbsoluteUri(relativeUri: string): string {
 
 export function hasDebuggingEnabled(bootConfig: BootJsonData): boolean {
     // Copied from blazor MonoDebugger.ts/attachDebuggerHotkey
+    if (!globalThis.navigator) {
+        return false;
+    }
 
     const hasReferencedPdbs = !!bootConfig.resources.pdb;
-    const debugBuild = bootConfig.debugBuild;
-
-    return (hasReferencedPdbs || debugBuild) && (loaderHelpers.isChromium || loaderHelpers.isFirefox);
+    return (hasReferencedPdbs || bootConfig.debugBuild || bootConfig.debugLevel != 0) && (loaderHelpers.isChromium || loaderHelpers.isFirefox);
 }
\ No newline at end of file
index 695911e..4d997a1 100644 (file)
@@ -94,6 +94,7 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi
             const loadedAnyConfig: any = (await configResponse.json()) || {};
             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);
             } else {
                 // Otherwise we found mono config schema
@@ -118,7 +119,7 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi
         }
         loaderHelpers.afterConfigLoaded.promise_control.resolve(loaderHelpers.config);
     } catch (err) {
-        const errMessage = `Failed to load config file ${configFilePath} ${err}`;
+        const errMessage = `Failed to load config file ${configFilePath} ${err} ${(err as Error)?.stack}`;
         loaderHelpers.config = module.config = <any>{ message: errMessage, error: err, isError: true };
         loaderHelpers.abort_startup(errMessage, true);
         throw err;
index 3c4315b..ae59010 100644 (file)
@@ -73,7 +73,10 @@ export async function fetch_like(url: string, init?: RequestInit): Promise<Respo
             const arrayBuffer = await node_fs.promises.readFile(url);
             return <Response><any>{
                 ok: true,
-                headers: [],
+                headers: {
+                    length: 0,
+                    get: () => null
+                },
                 url,
                 arrayBuffer: () => arrayBuffer,
                 json: () => JSON.parse(arrayBuffer),
@@ -89,6 +92,10 @@ export async function fetch_like(url: string, init?: RequestInit): Promise<Respo
             return <Response><any>{
                 ok: true,
                 url,
+                headers: {
+                    length: 0,
+                    get: () => null
+                },
                 arrayBuffer: () => {
                     return new Uint8Array(read(url, "binary"));
                 },
@@ -104,6 +111,10 @@ export async function fetch_like(url: string, init?: RequestInit): Promise<Respo
             ok: false,
             url,
             status: 500,
+            headers: {
+                length: 0,
+                get: () => null
+            },
             statusText: "ERR28: " + e,
             arrayBuffer: () => { throw e; },
             json: () => { throw e; },
index a147f9f..6d8b480 100644 (file)
@@ -395,13 +395,7 @@ async function createEmscriptenMain(): Promise<RuntimeAPI> {
 
     if (!module.configSrc && (!module.config || Object.keys(module.config).length === 0 || !module.config.assets)) {
         // if config file location nor assets are provided
-        if (loaderHelpers.scriptDirectory.indexOf("/_framework") == -1) {
-            // we are not inside _framework (= wasm template)
-            module.configSrc = "./_framework/blazor.boot.json";
-        } else {
-            // blazor app
-            module.configSrc = "./blazor.boot.json";
-        }
+        module.configSrc = "./blazor.boot.json";
     }
 
     // download config
index 1369a3f..b9dbdae 100644 (file)
@@ -117,7 +117,7 @@ export function afterLoadWasmModuleToWorker(worker: Worker): void {
 export function preAllocatePThreadWorkerPool(defaultPthreadPoolSize: number, config: MonoConfig): void {
     const poolSizeSpec = config?.pthreadPoolSize;
     let n: number;
-    if (poolSizeSpec === undefined) {
+    if (poolSizeSpec === undefined || poolSizeSpec === null) {
         n = defaultPthreadPoolSize;
     } else {
         mono_assert(typeof poolSizeSpec === "number", "pthreadPoolSize must be a number");
index b0accc8..20cfb1a 100644 (file)
@@ -7,12 +7,16 @@ export interface BootJsonData {
     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;
 
     // These properties are tacked on, and not found in the boot.json file
     modifiableAssemblies: string | null;
@@ -22,6 +26,7 @@ export interface BootJsonData {
 export type BootJsonDataExtension = { [extensionName: string]: ResourceList };
 
 export interface ResourceGroups {
+    readonly hash?: string;
     readonly assembly: ResourceList;
     readonly lazyAssembly: ResourceList;
     readonly pdb?: ResourceList;
@@ -30,6 +35,7 @@ export interface ResourceGroups {
     readonly libraryInitializers?: ResourceList,
     readonly extensions?: BootJsonDataExtension
     readonly runtimeAssets: ExtendedResourceList;
+    readonly vfs?: { [virtualPath: string]: ResourceList };
 }
 
 export type ResourceList = { [name: string]: string };
index 992fea2..735e7c4 100644 (file)
@@ -76,7 +76,8 @@ export type MonoConfigInternal = MonoConfig & {
     forwardConsoleLogsToWS?: boolean,
     asyncFlushOnExit?: boolean
     exitAfterSnapshot?: number,
-    startupOptions?: Partial<WebAssemblyStartOptions>
+    startupOptions?: Partial<WebAssemblyStartOptions>,
+    loadAllSatelliteResources?: boolean
 };
 
 export type RunArguments = {
index 6d9bd43..a073fc9 100644 (file)
@@ -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 { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
     .withDiagnosticTracing(false)
index 6dd163a..ea131be 100644 (file)
@@ -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 { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
     .withDiagnosticTracing(false)
index 1c82380..a6cd146 100644 (file)
@@ -4,7 +4,7 @@
 //
 // Run runtime tests under a JS shell or a browser
 //
-import { dotnet, exit } from './dotnet.js';
+import { dotnet, exit } from './_framework/dotnet.js';
 
 
 /*****************************************************************************
@@ -258,7 +258,10 @@ function configureRuntime(dotnet, runArgs) {
         .withDiagnosticTracing(runArgs.diagnosticTracing)
         .withExitOnUnhandledError()
         .withExitCodeLogging()
-        .withElementOnExit();
+        .withElementOnExit()
+        .withConfig({
+            loadAllSatelliteResources: true
+        });
 
     if (is_node) {
         dotnet
@@ -296,7 +299,7 @@ async function dry_run(runArgs) {
     try {
         console.log("Silently starting separate runtime instance as another ES6 module to populate caches...");
         // this separate instance of the ES6 module, in which we just populate the caches
-        const { dotnet } = await import('./dotnet.js?dry_run=true');
+        const { dotnet } = await import('./_framework/dotnet.js?dry_run=true');
         configureRuntime(dotnet, runArgs);
         // silent minimal startup
         await dotnet.withConfig({
index 04068ec..bf4beb0 100644 (file)
@@ -7,6 +7,8 @@ using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<str
 
 namespace Microsoft.NET.Sdk.WebAssembly;
 
+#nullable disable
+
 /// <summary>
 /// Defines the structure of a Blazor boot JSON file
 /// </summary>
@@ -40,6 +42,11 @@ public class BootJsonData
     public bool debugBuild { get; set; }
 
     /// <summary>
+    /// Gets a value that determines what level of debugging is configured.
+    /// </summary>
+    public int debugLevel { get; set; }
+
+    /// <summary>
     /// Gets a value that determines if the linker is enabled.
     /// </summary>
     public bool linkerEnabled { get; set; }
@@ -68,11 +75,31 @@ public class BootJsonData
     /// Gets or sets configuration extensions.
     /// </summary>
     public Dictionary<string, Dictionary<string, object>> extensions { get; set; }
+
+    /// <summary>
+    /// Gets or sets environment variables.
+    /// </summary>
+    public object environmentVariables { get; set; }
+
+    /// <summary>
+    /// Gets or sets diagnostic tracing.
+    /// </summary>
+    public object diagnosticTracing { get; set; }
+
+    /// <summary>
+    /// Gets or sets pthread pool size.
+    /// </summary>
+    public int? pthreadPoolSize { get; set; }
 }
 
 public class ResourcesData
 {
     /// <summary>
+    /// Gets a hash of all resources
+    /// </summary>
+    public string hash { get; set; }
+
+    /// <summary>
     /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc.
     /// </summary>
     public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary();
@@ -119,6 +146,12 @@ public class ResourcesData
     [DataMember(EmitDefaultValue = false)]
     public Dictionary<string, AdditionalAsset> runtimeAssets { get; set; }
 
+    [DataMember(EmitDefaultValue = false)]
+    public Dictionary<string, ResourceHashesByNameDictionary> vfs { get; set; }
+
+    [DataMember(EmitDefaultValue = false)]
+    public List<string> remoteSources { get; set; }
+
 }
 
 public enum ICUDataMode : int
@@ -155,8 +188,8 @@ public enum ICUDataMode : int
 public class AdditionalAsset
 {
     [DataMember(Name = "hash")]
-    public string Hash { get; set; }
+    public string hash { get; set; }
 
     [DataMember(Name = "behavior")]
-    public string Behavior { get; set; }
+    public string behavior { get; set; }
 }
index 699d346..9690097 100644 (file)
@@ -103,6 +103,7 @@ public class GenerateWasmBootJson : Task
             entryAssembly = entryAssemblyName,
             cacheBootResources = CacheBootResources,
             debugBuild = DebugBuild,
+            debugLevel = DebugBuild ? 1 : 0,
             linkerEnabled = LinkerEnabled,
             resources = new ResourcesData(),
             config = new List<string>(),
@@ -334,8 +335,8 @@ public class GenerateWasmBootJson : Task
             Log.LogMessage(MessageImportance.Low, "Added resource '{0}' to the list of additional assets in the manifest.", resource.ItemSpec);
             additionalResources.Add(resourceName, new AdditionalAsset
             {
-                Hash = $"sha256-{resource.GetMetadata("FileHash")}",
-                Behavior = behavior
+                hash = $"sha256-{resource.GetMetadata("FileHash")}",
+                behavior = behavior
             });
         }
     }
index 8f18345..88cf5fa 100644 (file)
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
@@ -13,6 +13,7 @@ using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 using Microsoft.Build.Framework;
 using Microsoft.Build.Utilities;
+using Microsoft.NET.Sdk.WebAssembly;
 
 namespace Microsoft.WebAssembly.Build.Tasks;
 
@@ -22,6 +23,9 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
     public bool IncludeThreadsWorker { get; set; }
     public int PThreadPoolSize { get; set; }
     public bool UseWebcil { get; set; }
+    public bool WasmIncludeFullIcuData { get; set; }
+    public string? WasmIcuDataFileName { get; set; }
+    public string? RuntimeAssetsLocation { get; set; }
 
     // <summary>
     // Extra json elements to add to _framework/blazor.boot.json
@@ -37,107 +41,6 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
     // </summary>
     public ITaskItem[]? ExtraConfig { get; set; }
 
-    private sealed class WasmAppConfig
-    {
-        [JsonPropertyName("mainAssemblyName")]
-        public string? MainAssemblyName { get; set; }
-        [JsonPropertyName("assemblyRootFolder")]
-        public string AssemblyRootFolder { get; set; } = "managed";
-        [JsonPropertyName("debugLevel")]
-        public int DebugLevel { get; set; } = 0;
-        [JsonPropertyName("assets")]
-        public List<object> Assets { get; } = new List<object>();
-        [JsonPropertyName("remoteSources")]
-        public List<string> RemoteSources { get; set; } = new List<string>();
-        [JsonPropertyName("globalizationMode")]
-        public string? GlobalizationMode { get; set; }
-        [JsonExtensionData]
-        public Dictionary<string, object?> Extra { get; set; } = new();
-        [JsonPropertyName("assetsHash")]
-        public string AssetsHash { get; set; } = "none";
-    }
-
-    private class AssetEntry
-    {
-        protected AssetEntry(string name, string hash, string behavior)
-        {
-            Name = name;
-            Behavior = behavior;
-            Hash = hash;
-        }
-        [JsonPropertyName("behavior")]
-        public string Behavior { get; init; }
-        [JsonPropertyName("name")]
-        public string Name { get; init; }
-        [JsonPropertyName("hash")]
-        public string? Hash { get; set; }
-    }
-
-    private sealed class WasmEntry : AssetEntry
-    {
-        public WasmEntry(string name, string hash) : base(name, hash, "dotnetwasm") { }
-    }
-
-    private sealed class LoaderJsEntry : AssetEntry
-    {
-        public LoaderJsEntry(string name, string hash) : base(name, hash, "js-module-dotnet") { }
-    }
-
-    private sealed class NativeJsEntry : AssetEntry
-    {
-        public NativeJsEntry(string name, string hash) : base(name, hash, "js-module-native") { }
-    }
-
-    private sealed class RuntimeJsEntry : AssetEntry
-    {
-        public RuntimeJsEntry(string name, string hash) : base(name, hash, "js-module-runtime") { }
-    }
-
-    private sealed class ThreadsWorkerEntry : AssetEntry
-    {
-        public ThreadsWorkerEntry(string name, string hash) : base(name, hash, "js-module-threads") { }
-    }
-
-    private sealed class AssemblyEntry : AssetEntry
-    {
-        public AssemblyEntry(string name, string hash) : base(name, hash, "assembly") { }
-    }
-
-    private sealed class PdbEntry : AssetEntry
-    {
-        public PdbEntry(string name, string hash) : base(name, hash, "pdb") { }
-    }
-
-    private sealed class SatelliteAssemblyEntry : AssetEntry
-    {
-        public SatelliteAssemblyEntry(string name, string hash, string culture) : base(name, hash, "resource")
-        {
-            CultureName = culture;
-        }
-
-        [JsonPropertyName("culture")]
-        public string CultureName { get; set; }
-    }
-
-    private sealed class VfsEntry : AssetEntry
-    {
-        public VfsEntry(string name, string hash) : base(name, hash, "vfs") { }
-        [JsonPropertyName("virtualPath")]
-        public string? VirtualPath { get; set; }
-    }
-
-    private sealed class IcuData : AssetEntry
-    {
-        public IcuData(string name, string hash) : base(name, hash, "icu") { }
-        [JsonPropertyName("loadRemote")]
-        public bool LoadRemote { get; set; }
-    }
-
-    private sealed class SymbolsData : AssetEntry
-    {
-        public SymbolsData(string name, string hash) : base(name, hash, "symbols") { }
-    }
-
     protected override bool ValidateArguments()
     {
         if (!base.ValidateArguments())
@@ -155,6 +58,28 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
         return true;
     }
 
+    private ICUDataMode GetICUDataMode()
+    {
+        // Invariant has always precedence
+        if (InvariantGlobalization)
+            return ICUDataMode.Invariant;
+
+        // If user provided a path to a custom ICU data file, use it
+        if (!string.IsNullOrEmpty(WasmIcuDataFileName))
+            return ICUDataMode.Custom;
+
+        // Hybrid mode
+        if (HybridGlobalization)
+            return ICUDataMode.Hybrid;
+
+        // If user requested to include full ICU data, use it
+        if (WasmIncludeFullIcuData)
+            return ICUDataMode.All;
+
+        // Otherwise, use sharded mode
+        return ICUDataMode.Sharded;
+    }
+
     protected override bool ExecuteInternal()
     {
         if (!ValidateArguments())
@@ -168,18 +93,26 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
         }
         MainAssemblyName = Path.GetFileName(MainAssemblyName);
 
-        var config = new WasmAppConfig()
+        var bootConfig = new BootJsonData()
         {
-            MainAssemblyName = MainAssemblyName,
-            GlobalizationMode = InvariantGlobalization ? "invariant" : HybridGlobalization ? "hybrid" : "icu"
+            config = new(),
+            entryAssembly = MainAssemblyName,
+            icuDataMode = GetICUDataMode()
         };
 
         // Create app
-        var asmRootPath = Path.Combine(AppDir, config.AssemblyRootFolder);
+        var runtimeAssetsPath = !string.IsNullOrEmpty(RuntimeAssetsLocation)
+            ? Path.Combine(AppDir, RuntimeAssetsLocation)
+            : AppDir;
+
+        Log.LogMessage(MessageImportance.Low, $"Runtime assets output path {runtimeAssetsPath}");
+
         Directory.CreateDirectory(AppDir!);
-        Directory.CreateDirectory(asmRootPath);
+        Directory.CreateDirectory(runtimeAssetsPath);
+
         if (UseWebcil)
             Log.LogMessage(MessageImportance.Normal, "Converting assemblies to Webcil");
+
         foreach (var assembly in _assemblies)
         {
             if (UseWebcil)
@@ -187,7 +120,7 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
                 var tmpWebcil = Path.GetTempFileName();
                 var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: assembly, outputPath: tmpWebcil, logger: Log);
                 webcilWriter.ConvertToWebcil();
-                var finalWebcil = Path.Combine(asmRootPath, Path.ChangeExtension(Path.GetFileName(assembly), Utils.WebcilInWasmExtension));
+                var finalWebcil = Path.Combine(runtimeAssetsPath, Path.ChangeExtension(Path.GetFileName(assembly), Utils.WebcilInWasmExtension));
                 if (Utils.CopyIfDifferent(tmpWebcil, finalWebcil, useHash: true))
                     Log.LogMessage(MessageImportance.Low, $"Generated {finalWebcil} .");
                 else
@@ -196,47 +129,42 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
             }
             else
             {
-                FileCopyChecked(assembly, Path.Combine(asmRootPath, Path.GetFileName(assembly)), "Assemblies");
+                FileCopyChecked(assembly, Path.Combine(runtimeAssetsPath, Path.GetFileName(assembly)), "Assemblies");
             }
             if (DebugLevel != 0)
             {
                 var pdb = assembly;
                 pdb = Path.ChangeExtension(pdb, ".pdb");
                 if (File.Exists(pdb))
-                    FileCopyChecked(pdb, Path.Combine(asmRootPath, Path.GetFileName(pdb)), "Assemblies");
+                    FileCopyChecked(pdb, Path.Combine(runtimeAssetsPath, Path.GetFileName(pdb)), "Assemblies");
             }
         }
 
         foreach (ITaskItem item in NativeAssets)
         {
             var name = Path.GetFileName(item.ItemSpec);
-            var dest = Path.Combine(AppDir!, name);
+            var dest = Path.Combine(runtimeAssetsPath, name);
             if (!FileCopyChecked(item.ItemSpec, dest, "NativeAssets"))
                 return false;
-            if (name == "dotnet.js")
-            {
-                config.Assets.Add(new LoaderJsEntry (name, Utils.ComputeIntegrity(item.ItemSpec)) );
-            }
-            else if (name == "dotnet.native.wasm")
-            {
-                config.Assets.Add(new WasmEntry(name, Utils.ComputeIntegrity(item.ItemSpec)));
-            }
-            else if (name == "dotnet.native.js")
-            {
-                config.Assets.Add(new NativeJsEntry (name, Utils.ComputeIntegrity(item.ItemSpec)) );
-            }
-            else if (name == "dotnet.runtime.js")
-            {
-                config.Assets.Add(new RuntimeJsEntry (name, Utils.ComputeIntegrity(item.ItemSpec)) );
-            }
-            else if (IncludeThreadsWorker && name == "dotnet.native.worker.js")
-            {
-                config.Assets.Add(new ThreadsWorkerEntry(name, Utils.ComputeIntegrity(item.ItemSpec)));
-            }
-            else if (name == "dotnet.native.js.symbols")
+
+            if (!IncludeThreadsWorker && name == "dotnet.native.worker.js")
+                continue;
+
+            var itemHash = Utils.ComputeIntegrity(item.ItemSpec);
+
+            if (name.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(Path.GetExtension(name), ".wasm", StringComparison.OrdinalIgnoreCase))
             {
-                config.Assets.Add(new SymbolsData(name, Utils.ComputeIntegrity(item.ItemSpec)));
+                if (bootConfig.resources.runtimeAssets == null)
+                    bootConfig.resources.runtimeAssets = new();
+
+                bootConfig.resources.runtimeAssets[name] = new()
+                {
+                    hash = itemHash,
+                    behavior = "dotnetwasm"
+                };
             }
+
+            bootConfig.resources.runtime[name] = itemHash;
         }
 
         string packageJsonPath = Path.Combine(AppDir, "package.json");
@@ -259,57 +187,74 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
             {
                 if (UseWebcil)
                 {
-                    assemblyPath = Path.Combine(asmRootPath, Path.ChangeExtension(Path.GetFileName(assembly), Utils.WebcilInWasmExtension));
+                    assemblyPath = Path.Combine(runtimeAssetsPath, Path.ChangeExtension(Path.GetFileName(assembly), Utils.WebcilInWasmExtension));
                     // For the hash, read the bytes from the webcil file, not the dll file.
                     bytes = File.ReadAllBytes(assemblyPath);
                 }
 
-                config.Assets.Add(new AssemblyEntry(Path.GetFileName(assemblyPath), Utils.ComputeIntegrity(bytes)));
+                bootConfig.resources.assembly[Path.GetFileName(assemblyPath)] = Utils.ComputeIntegrity(bytes);
                 if (DebugLevel != 0)
                 {
-                    var pdb = assembly;
-                    pdb = Path.ChangeExtension(pdb, ".pdb");
+                    var pdb = Path.ChangeExtension(assembly, ".pdb");
                     if (File.Exists(pdb))
-                        config.Assets.Add(new PdbEntry(Path.GetFileName(pdb), Utils.ComputeIntegrity(pdb)));
+                    {
+                        if (bootConfig.resources.pdb == null)
+                            bootConfig.resources.pdb = new();
+
+                        bootConfig.resources.pdb[Path.GetFileName(pdb)] = Utils.ComputeIntegrity(pdb);
+                    }
                 }
             }
         }
 
-        config.DebugLevel = DebugLevel;
+        bootConfig.debugBuild = DebugLevel > 0;
+        bootConfig.debugLevel = DebugLevel;
 
         ProcessSatelliteAssemblies(args =>
         {
+            if (bootConfig.resources.satelliteResources == null)
+                bootConfig.resources.satelliteResources = new();
+
             string name = Path.GetFileName(args.fullPath);
-            string directory = Path.Combine(AppDir, config.AssemblyRootFolder, args.culture);
-            Directory.CreateDirectory(directory);
+            string cultureDirectory = Path.Combine(runtimeAssetsPath, args.culture);
+            Directory.CreateDirectory(cultureDirectory);
             if (UseWebcil)
             {
                 var tmpWebcil = Path.GetTempFileName();
                 var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: args.fullPath, outputPath: tmpWebcil, logger: Log);
                 webcilWriter.ConvertToWebcil();
-                var finalWebcil = Path.Combine(directory, Path.ChangeExtension(name, Utils.WebcilInWasmExtension));
+                var finalWebcil = Path.Combine(cultureDirectory, Path.ChangeExtension(name, Utils.WebcilInWasmExtension));
                 if (Utils.CopyIfDifferent(tmpWebcil, finalWebcil, useHash: true))
                     Log.LogMessage(MessageImportance.Low, $"Generated {finalWebcil} .");
                 else
                     Log.LogMessage(MessageImportance.Low, $"Skipped generating {finalWebcil} as the contents are unchanged.");
                 _fileWrites.Add(finalWebcil);
-                config.Assets.Add(new SatelliteAssemblyEntry(Path.GetFileName(finalWebcil), Utils.ComputeIntegrity(finalWebcil), args.culture));
+
+                if (!bootConfig.resources.satelliteResources.TryGetValue(args.culture, out var cultureSatelliteResources))
+                    bootConfig.resources.satelliteResources[args.culture] = cultureSatelliteResources = new();
+
+                cultureSatelliteResources[Path.GetFileName(finalWebcil)] = Utils.ComputeIntegrity(finalWebcil);
             }
             else
             {
-                var satellitePath = Path.Combine(directory, name);
+                var satellitePath = Path.Combine(cultureDirectory, name);
                 FileCopyChecked(args.fullPath, satellitePath, "SatelliteAssemblies");
-                config.Assets.Add(new SatelliteAssemblyEntry(name, Utils.ComputeIntegrity(satellitePath), args.culture));
+
+                if (!bootConfig.resources.satelliteResources.TryGetValue(args.culture, out var cultureSatelliteResources))
+                    bootConfig.resources.satelliteResources[args.culture] = cultureSatelliteResources = new();
+
+                cultureSatelliteResources[name] = Utils.ComputeIntegrity(satellitePath);
             }
         });
 
         if (FilesToIncludeInFileSystem.Length > 0)
         {
-            string supportFilesDir = Path.Combine(AppDir, "supportFiles");
+            string supportFilesDir = Path.Combine(runtimeAssetsPath, "supportFiles");
             Directory.CreateDirectory(supportFilesDir);
 
             var i = 0;
             StringDictionary targetPathTable = new();
+            var vfs = new Dictionary<string, Dictionary<string, string>>();
             foreach (var item in FilesToIncludeInFileSystem)
             {
                 string? targetPath = item.GetMetadata("TargetPath");
@@ -340,12 +285,14 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
                 var vfsPath = Path.Combine(supportFilesDir, generatedFileName);
                 FileCopyChecked(item.ItemSpec, vfsPath, "FilesToIncludeInFileSystem");
 
-                var asset = new VfsEntry($"supportFiles/{generatedFileName}", Utils.ComputeIntegrity(vfsPath))
+                vfs[targetPath] = new()
                 {
-                    VirtualPath = targetPath
+                    [$"supportFiles/{generatedFileName}"] = Utils.ComputeIntegrity(vfsPath)
                 };
-                config.Assets.Add(asset);
             }
+
+            if (vfs.Count > 0)
+                bootConfig.resources.vfs = vfs;
         }
 
         if (!InvariantGlobalization)
@@ -358,25 +305,29 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
                     Log.LogError($"Expected the file defined as ICU resource: {idfn} to exist but it does not.");
                     return false;
                 }
-                config.Assets.Add(new IcuData(Path.GetFileName(idfn), Utils.ComputeIntegrity(idfn)) { LoadRemote = loadRemote });
+
+                bootConfig.resources.runtime[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn);
             }
         }
 
 
         if (RemoteSources?.Length > 0)
         {
+            bootConfig.resources.remoteSources = new();
             foreach (var source in RemoteSources)
                 if (source != null && source.ItemSpec != null)
-                    config.RemoteSources.Add(source.ItemSpec);
+                    bootConfig.resources.remoteSources.Add(source.ItemSpec);
         }
 
+        var extraConfiguration = new Dictionary<string, object?>();
+
         if (PThreadPoolSize < -1)
         {
             throw new LogAsErrorException($"PThreadPoolSize must be -1, 0 or positive, but got {PThreadPoolSize}");
         }
         else if (PThreadPoolSize > -1)
         {
-            config.Extra["pthreadPoolSize"] = PThreadPoolSize;
+            bootConfig.pthreadPoolSize = PThreadPoolSize;
         }
 
         foreach (ITaskItem extra in ExtraConfig ?? Enumerable.Empty<ITaskItem>())
@@ -385,28 +336,67 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
             if (!TryParseExtraConfigValue(extra, out object? valueObject))
                 return false;
 
-            config.Extra[name] = valueObject;
+            if (string.Equals(name, nameof(BootJsonData.environmentVariables), StringComparison.OrdinalIgnoreCase))
+            {
+                bootConfig.environmentVariables = valueObject;
+            }
+            else if (string.Equals(name, nameof(BootJsonData.diagnosticTracing), StringComparison.OrdinalIgnoreCase))
+            {
+                if (valueObject is bool boolValue || (valueObject is string stringValue && bool.TryParse(stringValue, out boolValue)))
+                    bootConfig.diagnosticTracing = boolValue;
+                else
+                    throw new LogAsErrorException($"Unsupported value '{valueObject}' of type '{valueObject?.GetType()?.FullName}' for extra config 'diagnosticTracing'.");
+            }
+            else
+            {
+                extraConfiguration[name] = valueObject;
+            }
+        }
+
+        if (extraConfiguration.Count > 0)
+        {
+            bootConfig.extensions = new()
+            {
+                ["extra"] = extraConfiguration
+            };
         }
 
         string tmpMonoConfigPath = Path.GetTempFileName();
         using (var sw = File.CreateText(tmpMonoConfigPath))
         {
             var sb = new StringBuilder();
-            foreach (AssetEntry asset in config.Assets)
+
+            static void AddDictionary(StringBuilder sb, Dictionary<string, string> res)
             {
-                sb.Append(asset.Hash);
+                foreach (var asset in res)
+                    sb.Append(asset.Value);
             }
-            config.AssetsHash = Utils.ComputeTextIntegrity(sb.ToString());
 
-            var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
+            AddDictionary(sb, bootConfig.resources.assembly);
+            AddDictionary(sb, bootConfig.resources.runtime);
+
+            if (bootConfig.resources.lazyAssembly != null)
+                AddDictionary(sb, bootConfig.resources.lazyAssembly);
+
+            if (bootConfig.resources.satelliteResources != null)
+            {
+                foreach (var culture in bootConfig.resources.satelliteResources)
+                    AddDictionary(sb, culture.Value);
+            }
+
+            if (bootConfig.resources.vfs != null)
+            {
+                foreach (var entry in bootConfig.resources.vfs)
+                    AddDictionary(sb, entry.Value);
+            }
+
+            bootConfig.resources.hash = Utils.ComputeTextIntegrity(sb.ToString());
+
+            var json = JsonSerializer.Serialize(bootConfig, new JsonSerializerOptions { WriteIndented = true });
             sw.Write(json);
         }
 
-        string monoConfigDir = Path.Combine(AppDir, "_framework");
-        if (!Directory.Exists(monoConfigDir))
-            Directory.CreateDirectory(monoConfigDir);
-
-        string monoConfigPath = Path.Combine(monoConfigDir, "blazor.boot.json"); // TODO: Unify with Wasm SDK
+        string monoConfigPath = Path.Combine(runtimeAssetsPath, "blazor.boot.json"); // TODO: Unify with Wasm SDK
         Utils.CopyIfDifferent(tmpMonoConfigPath, monoConfigPath, useHash: false);
         _fileWrites.Add(monoConfigPath);
 
index 320586d..a873b4d 100644 (file)
@@ -19,6 +19,7 @@
   <ItemGroup>
     <Compile Include="..\Common\Utils.cs" />
     <Compile Include="..\Common\LogAsErrorException.cs" />
+    <Compile Include="..\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\BootJsonData.cs" />
 
     <PackageReference Include="Microsoft.Build" Version="$(MicrosoftBuildVersion)" />
     <PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(MicrosoftBuildTasksCoreVersion)" />
index ed7d0f2..b1ca242 100644 (file)
@@ -1,4 +1,4 @@
-import { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 function wasm_exit(exit_code) {
     var tests_done_elem = document.createElement("label");
index 249aa44..7ca0a21 100644 (file)
@@ -1,4 +1,4 @@
-import { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 function wasm_exit(exit_code) {
     var tests_done_elem = document.createElement("label");
index a7323e1..10858c7 100644 (file)
@@ -1,4 +1,4 @@
-import { dotnet } from './dotnet.js'
+import { dotnet } from './_framework/dotnet.js'
 
 function wasm_exit(exit_code) {
     var tests_done_elem = document.createElement("label");