From f564d442ea83f645b672aa103fa31a7aec7f3c8b Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 8 May 2026 22:32:19 +0300 Subject: [PATCH 01/19] remove embed and mt modes --- .../Pack/BindingTest.cs | 2 +- .../Pack/DeclarationTest.cs | 7 - .../Bootsharp.Publish.Test/Pack/PackTest.cs | 11 +- .../Pack/PatcherTest.cs | 35 --- .../Pack/ResourceTest.cs | 32 +-- .../Common/Global/GlobalText.cs | 5 + .../Common/Inspector/SerializedInspector.cs | 2 +- .../Pack/BindingGenerator/BindingGenerator.cs | 11 +- .../Bootsharp.Publish/Pack/BootsharpPack.cs | 19 +- .../DeclarationGenerator.cs | 2 +- .../Bootsharp.Publish/Pack/ModulePatcher.cs | 36 +++ .../Pack/ModulePatcher/InternalPatcher.cs | 34 --- .../Pack/ModulePatcher/ModulePatcher.cs | 71 ----- .../Pack/ResourceGenerator.cs | 19 +- src/cs/Bootsharp/Build/Bootsharp.props | 6 +- src/cs/Bootsharp/Build/Bootsharp.targets | 55 ++-- src/cs/Bootsharp/Build/PackageTemplate.json | 2 +- src/cs/Directory.Build.props | 2 +- src/js/package.json | 20 +- src/js/scripts/build.sh | 5 +- src/js/scripts/compile-test.sh | 4 +- src/js/scripts/cover.sh | 3 - src/js/scripts/test.sh | 1 - src/js/src/bindings.g.ts | 2 - src/js/src/boot.mts | 74 +++++ src/js/src/boot.ts | 75 ----- src/js/src/config.mts | 49 ++++ src/js/src/config.ts | 88 ------ src/js/src/decoder.ts | 56 ---- src/js/src/dotnet.native.g.d.ts | 2 - src/js/src/dotnet.runtime.g.d.ts | 2 - src/js/src/dotnet/dotnet.d.ts | 2 + src/js/src/{ => dotnet}/dotnet.g.d.ts | 4 - src/js/src/dotnet/dotnet.native.d.ts | 2 + src/js/src/dotnet/dotnet.runtime.d.ts | 2 + src/js/src/dotnet/index.mts | 16 ++ src/js/src/{event.ts => event.mts} | 0 src/js/src/{exports.ts => exports.mts} | 2 +- src/js/src/generated/bindings.g.mts | 1 + src/js/src/generated/resources.g.mts | 3 + src/js/src/imports.mts | 10 + src/js/src/imports.ts | 10 - src/js/src/index.mts | 17 ++ src/js/src/index.ts | 19 -- src/js/src/{instances.ts => instances.mts} | 4 +- src/js/src/modules.ts | 30 -- src/js/src/resources.g.ts | 4 - src/js/src/resources.mts | 45 +++ src/js/src/resources.ts | 28 -- src/js/src/{runtime.ts => runtime.mts} | 2 +- .../{serialization.ts => serialization.mts} | 8 +- src/js/test/cs.ts | 96 ++----- src/js/test/cs/Test/Platform.cs | 7 + src/js/test/cs/Test/Test.csproj | 3 +- src/js/test/spec/boot.spec.ts | 270 +++++------------- src/js/test/spec/export.spec.ts | 14 +- src/js/test/spec/interop.spec.ts | 7 +- src/js/test/spec/platform.spec.ts | 6 +- src/js/test/spec/serialization.spec.ts | 20 +- src/js/test/tsconfig.json | 8 + src/js/tsconfig.json | 3 +- 61 files changed, 498 insertions(+), 877 deletions(-) delete mode 100644 src/cs/Bootsharp.Publish.Test/Pack/PatcherTest.cs create mode 100644 src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs delete mode 100644 src/js/scripts/cover.sh delete mode 100644 src/js/scripts/test.sh delete mode 100644 src/js/src/bindings.g.ts create mode 100644 src/js/src/boot.mts delete mode 100644 src/js/src/boot.ts create mode 100644 src/js/src/config.mts delete mode 100644 src/js/src/config.ts delete mode 100644 src/js/src/decoder.ts delete mode 100644 src/js/src/dotnet.native.g.d.ts delete mode 100644 src/js/src/dotnet.runtime.g.d.ts create mode 100644 src/js/src/dotnet/dotnet.d.ts rename src/js/src/{ => dotnet}/dotnet.g.d.ts (99%) create mode 100644 src/js/src/dotnet/dotnet.native.d.ts create mode 100644 src/js/src/dotnet/dotnet.runtime.d.ts create mode 100644 src/js/src/dotnet/index.mts rename src/js/src/{event.ts => event.mts} (100%) rename src/js/src/{exports.ts => exports.mts} (86%) create mode 100644 src/js/src/generated/bindings.g.mts create mode 100644 src/js/src/generated/resources.g.mts create mode 100644 src/js/src/imports.mts delete mode 100644 src/js/src/imports.ts create mode 100644 src/js/src/index.mts delete mode 100644 src/js/src/index.ts rename src/js/src/{instances.ts => instances.mts} (95%) delete mode 100644 src/js/src/modules.ts delete mode 100644 src/js/src/resources.g.ts create mode 100644 src/js/src/resources.mts delete mode 100644 src/js/src/resources.ts rename src/js/src/{runtime.ts => runtime.mts} (88%) rename src/js/src/{serialization.ts => serialization.mts} (97%) create mode 100644 src/js/test/tsconfig.json diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index e39f7a01..b316f102 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -855,7 +855,7 @@ class JSExported { get getImportedSerialized() { return this.getImportedSerializedHandler; } }; export const Exported = { - broadcastChangedSerialized(_id, arg1, arg2) { instances.export(_id, id => new JSExported(id)).broadcastChanged(instances.export(arg1, id => new JSExported(id)), deserialize(arg2, Info)); } + broadcastChangedSerialized: (_id, arg1, arg2) => instances.export(_id, /* v8 ignore next -- @preserve */ id => new JSExported(id)).broadcastChanged(instances.export(arg1, /* v8 ignore next -- @preserve */ id => new JSExported(id)), deserialize(arg2, Info)) }; """); } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 4ba940d2..c8bfa9f4 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -4,13 +4,6 @@ public class DeclarationTest : PackTest { protected override string TestedContent => GeneratedDeclarations; - [Fact] - public void ImportsEventTypes () - { - Execute(); - Contains("""import type { EventBroadcaster, EventSubscriber } from "./event";"""); - } - [Fact] public void DeclaresNamespace () { diff --git a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs index e3fd4eb1..2ca92d3c 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs @@ -7,12 +7,9 @@ public class PackTest : TaskTest protected string MockDotNetContent { get; } = "MockDotNetContent"; protected string MockRuntimeContent { get; } = "MockRuntimeContent"; protected string MockNativeContent { get; } = "MockNativeContent"; - protected string GeneratedBindings => ReadProjectFile("bindings.g.js"); - protected string GeneratedDeclarations => ReadProjectFile("bindings.g.d.ts"); - protected string GeneratedResources => ReadProjectFile("resources.g.js"); - protected string GeneratedDotNetModule => ReadProjectFile("dotnet.g.js"); - protected string GeneratedRuntimeModule => ReadProjectFile("dotnet.runtime.g.js"); - protected string GeneratedNativeModule => ReadProjectFile("dotnet.native.g.js"); + protected string GeneratedBindings => ReadProjectFile("generated/bindings.g.mjs"); + protected string GeneratedDeclarations => ReadProjectFile("generated/bindings.g.d.mts"); + protected string GeneratedResources => ReadProjectFile("generated/resources.g.mjs"); public PackTest () { @@ -44,9 +41,7 @@ protected override void AddAssembly (string assemblyName, params MockSource[] so InspectedDirectory = Project.Root, EntryAssemblyName = "System.Runtime.dll", BuildEngine = Engine, - EmbedBinaries = false, Globalization = false, - Threading = false, LLVM = false, Debug = false }; diff --git a/src/cs/Bootsharp.Publish.Test/Pack/PatcherTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/PatcherTest.cs deleted file mode 100644 index bb40a173..00000000 --- a/src/cs/Bootsharp.Publish.Test/Pack/PatcherTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Bootsharp.Publish.Test; - -public class PatcherTest : PackTest -{ - [Fact] - public void WhenEmbedEnabledModulesAreCopied () - { - Task.EmbedBinaries = true; - Execute(); - Assert.Equal(MockDotNetContent, GeneratedDotNetModule); - Assert.Equal(MockRuntimeContent, GeneratedRuntimeModule); - Assert.Equal(MockNativeContent, GeneratedNativeModule); - } - - [Fact] - public void WhenEmbedDisabledModuleExportFalseFlag () - { - Task.EmbedBinaries = false; - Execute(); - Assert.Equal("export const embedded = false;\nexport const mt = false;", GeneratedDotNetModule); - Assert.Equal("export const embedded = false;\nexport const mt = false;", GeneratedRuntimeModule); - Assert.Equal("export const embedded = false;\nexport const mt = false;", GeneratedNativeModule); - } - - [Fact] - public void WhenTreadingEnabledFlagIsSet () - { - Task.EmbedBinaries = false; - Task.Threading = true; - Execute(); - Assert.Equal("export const embedded = false;\nexport const mt = true;", GeneratedDotNetModule); - Assert.Equal("export const embedded = false;\nexport const mt = true;", GeneratedRuntimeModule); - Assert.Equal("export const embedded = false;\nexport const mt = true;", GeneratedNativeModule); - } -} diff --git a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs index ef6b7ab1..46b2dc53 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs @@ -12,26 +12,6 @@ public void EntryAssemblyNameIsWritten () Contains("entryAssemblyName: \"Foo.dll\""); } - [Fact] - public void BinariesEmbeddedWhenEnabled () - { - AddAssembly("Foo.dll"); - Task.EmbedBinaries = true; - Execute(); - Contains($$"""wasm: { name: "dotnet.native.wasm", content: "{{Convert.ToBase64String(MockWasmBinary)}}" },"""); - Contains("{ name: \"Foo.wasm\", content: \""); - } - - [Fact] - public void BinariesNotEmbeddedWhenDisabled () - { - AddAssembly("Foo.dll"); - Task.EmbedBinaries = false; - Execute(); - Contains("""wasm: { name: "dotnet.native.wasm", content: undefined },"""); - Contains("""{ name: "Foo.wasm", content: undefined"""); - } - [Fact] public void WhenDebugEnabledDebugArtifactsIncluded () { @@ -40,8 +20,8 @@ public void WhenDebugEnabledDebugArtifactsIncluded () Project.WriteFile("Foo.pdb", "MockPdbContent"); Project.WriteFile("dotnet.native.js.symbols", "MockSymbolsContent"); Execute(); - Contains("""{ name: "Foo.pdb", content: undefined }"""); - Contains("""{ name: "dotnet.native.js.symbols", content: undefined }"""); + Contains("""{ name: "Foo.pdb" }"""); + Contains("""{ name: "dotnet.native.js.symbols" }"""); } [Fact] @@ -52,8 +32,8 @@ public void WhenDebugDisabledDebugArtifactsNotIncluded () Project.WriteFile("Foo.pdb", "MockPdbContent"); Project.WriteFile("dotnet.native.js.symbols", "MockSymbolsContent"); Execute(); - DoesNotContain("""{ name: "Foo.pdb", content: undefined }"""); - DoesNotContain("""{ name: "dotnet.native.js.symbols", content: undefined }"""); + DoesNotContain("""{ name: "Foo.pdb" }"""); + DoesNotContain("""{ name: "dotnet.native.js.symbols" }"""); } [Fact] @@ -63,7 +43,7 @@ public void WhenGlobalizationEnabledIcuIncluded () AddAssembly("Foo.dll"); Project.WriteFile("icudt.dat", "MockIcuContent"); Execute(); - Contains("""{ name: "icudt.dat", content: undefined }"""); + Contains("""{ name: "icudt.dat" }"""); } [Fact] @@ -73,6 +53,6 @@ public void WhenGlobalizationDisabledIcuNotIncluded () AddAssembly("Foo.dll"); Project.WriteFile("icudt.dat", "MockIcuContent"); Execute(); - DoesNotContain("""{ name: "icudt.dat", content: undefined }"""); + DoesNotContain("""{ name: "icudt.dat" }"""); } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs index d9abe567..6dcfe512 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs @@ -20,4 +20,9 @@ public static string ToFirstLower (string value) if (value.Length == 1) return value.ToLowerInvariant(); return char.ToLowerInvariant(value[0]) + value[1..]; } + + public static string IgnoreV8 (this string content, string before) + { + return content.Replace(before, $"/* v8 ignore next -- @preserve */ {before}"); + } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs index 46aabb9a..b1fe07bd 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs @@ -97,7 +97,7 @@ private SerializedPropertyMeta BuildProperty (PropertyInfo prop, bool ctor) Info = prop, Name = prop.Name, JSName = BuildJSName(prop.Name), - OmitWhenNull = !prop.PropertyType.IsValueType || IsNullable(prop.PropertyType), + OmitWhenNull = IsNullable(prop.PropertyType, GetNullity(prop)), Required = prop.CustomAttributes .Any(a => a.AttributeType.FullName == typeof(RequiredMemberAttribute).FullName), ConstructorParameter = ctor, diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index 04b710f4..32bb7344 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -65,10 +65,10 @@ private void EmitImports () { bld.Append( """ - import { exports } from "./exports"; - import { Event } from "./event"; - import { instances } from "./instances"; - import { serialize, deserialize, binary, types } from "./serialization"; + import { exports } from "../exports.mjs"; + import { Event } from "../event.mjs"; + import { instances } from "../instances.mjs"; + import { serialize, deserialize, binary, types } from "../serialization.mjs"; """ ); } @@ -186,7 +186,8 @@ private void EmitEventExport (EventMeta evt) if (isIt) { var invName = $"instances.export(_id, id => new {it.JSName}(id)).broadcast{evt.Name}"; - bld.Append($"{Br}{name}({PrependIdArg(args)}) {{ {invName}({invArgs}); }}"); + bld.Append($"{Br}{name}: ({PrependIdArg(args)}) => {invName}({invArgs})" + .IgnoreV8("id =>")); // Uncoverable, as finalization in Node is not controllable. } else { diff --git a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs index aae6c9a3..957a039b 100644 --- a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs +++ b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs @@ -9,9 +9,7 @@ public sealed class BootsharpPack : Microsoft.Build.Utilities.Task public required string DebugDirectory { get; set; } public required string InspectedDirectory { get; set; } public required string EntryAssemblyName { get; set; } - public required bool EmbedBinaries { get; set; } public required bool Globalization { get; set; } - public required bool Threading { get; set; } public required bool LLVM { get; set; } public required bool Debug { get; set; } @@ -55,26 +53,33 @@ private void GenerateBindings (Preferences prefs, SolutionInspection spec) { var generator = new BindingGenerator(prefs, Debug); var content = generator.Generate(spec); - File.WriteAllText(Path.Combine(BuildDirectory, "bindings.g.js"), content); + WriteGenerated("bindings.g.mjs", content); } private void GenerateDeclarations (Preferences prefs, SolutionInspection spec) { var generator = new DeclarationGenerator(prefs); var content = generator.Generate(spec); - File.WriteAllText(Path.Combine(BuildDirectory, "bindings.g.d.ts"), content); + WriteGenerated("bindings.g.d.mts", content); } private void GenerateResources (SolutionInspection spec) { - var generator = new ResourceGenerator(EntryAssemblyName, EmbedBinaries, Debug, Globalization); + var generator = new ResourceGenerator(EntryAssemblyName, Debug, Globalization); var content = generator.Generate(BuildDirectory, DebugDirectory); - File.WriteAllText(Path.Combine(BuildDirectory, "resources.g.js"), content); + WriteGenerated("resources.g.mjs", content); } private void PatchModules () { - var patcher = new ModulePatcher(BuildDirectory, Threading, EmbedBinaries); + var patcher = new ModulePatcher(BuildDirectory); patcher.Patch(); } + + private void WriteGenerated (string filename, string content) + { + var dir = Path.Combine(BuildDirectory, "generated"); + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, filename), content); + } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs index 68431743..3595855d 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs @@ -6,7 +6,7 @@ internal sealed class DeclarationGenerator (Preferences prefs) private readonly TypeDeclarationGenerator types = new(prefs); public string Generate (SolutionInspection spec) => Fmt(0, - """import type { EventBroadcaster, EventSubscriber } from "./event";""", + """import type { EventBroadcaster, EventSubscriber } from "../event.mjs";""", types.Generate(spec), modules.Generate(spec) ) + "\n"; diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs new file mode 100644 index 00000000..b870b9ee --- /dev/null +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs @@ -0,0 +1,36 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Bootsharp.Publish; + +internal sealed class ModulePatcher (string buildDir) +{ + private readonly string dotnet = Path.Combine(buildDir, "dotnet.js"); + private readonly string runtime = Path.Combine(buildDir, "dotnet.runtime.js"); + private readonly string native = Path.Combine(buildDir, "dotnet.native.js"); + + public void Patch () + { + RemoveMaps(); + RemoveWasmNag(); + } + + private void RemoveMaps () + { + // Microsoft bundles .NET JavaScript sources pre-minified/uglified with source maps + // referencing upstream sources we don't publish with the package. + var regex = new Regex(@"^\s*//# sourceMappingURL=.*?\.map\s*$\r?\n?", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline); + File.WriteAllText(dotnet, regex.Replace(File.ReadAllText(dotnet, Encoding.UTF8), ""), Encoding.UTF8); + File.WriteAllText(runtime, regex.Replace(File.ReadAllText(runtime, Encoding.UTF8), ""), Encoding.UTF8); + File.WriteAllText(native, regex.Replace(File.ReadAllText(native, Encoding.UTF8), ""), Encoding.UTF8); + } + + private void RemoveWasmNag () + { + // Removes "WebAssembly resource does not have the expected content type..." warning. + File.WriteAllText(dotnet, new Regex("""(?:[$\w]+\.)*[$\w]+\(\s*(['"])WebAssembly resource does not have the expected content type \\?"application/wasm\\?", so falling back to slower ArrayBuffer instantiation\.\1\s*\)""", + RegexOptions.Compiled | RegexOptions.CultureInvariant) + .Replace(File.ReadAllText(dotnet, Encoding.UTF8), "true"), Encoding.UTF8); + } +} diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs deleted file mode 100644 index 8bf75f07..00000000 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text; - -namespace Bootsharp.Publish; - -internal sealed class InternalPatcher (string dotnet, string runtime, string native) -{ - private const string url = - """ - ((typeof window === "object" && "Deno" in window && Deno.build.os === "windows") || (typeof process === "object" && process.platform === "win32")) ? "file://dotnet.native.wasm" : "file:///dotnet.native.wasm" - """; - - public void Patch () - { - // Remove unnecessary environment-specific calls in .NET's internals, - // that are offending bundlers and breaking usage in restricted environments, - // such as VS Code web extensions. (https://github.com/elringus/bootsharp/issues/139) - - File.WriteAllText(dotnet, File.ReadAllText(dotnet, Encoding.UTF8) - .Replace("import.meta.url", url) - .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); - - File.WriteAllText(runtime, File.ReadAllText(runtime, Encoding.UTF8) - .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); - - File.WriteAllText(native, File.ReadAllText(native, Encoding.UTF8) - .Replace("var _scriptDir = import.meta.url", "var _scriptDir = \"file:/\"") - .Replace("require('url').fileURLToPath(new URL('./', import.meta.url))", "\"./\"") - .Replace("require(\"url\").fileURLToPath(new URL(\"./\",import.meta.url))", "\"./\"") // when aggressive trimming enabled - .Replace("new URL('dotnet.native.wasm', import.meta.url).href", "\"file:/\"") - .Replace("new URL(\"dotnet.native.wasm\",import.meta.url).href", "\"file:/\"") // when aggressive trimming enabled - .Replace("import.meta.url", url) - .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); - } -} diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs deleted file mode 100644 index af06e685..00000000 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text; -using System.Text.RegularExpressions; - -namespace Bootsharp.Publish; - -internal sealed class ModulePatcher (string buildDir, bool thread, bool embed) -{ - private readonly string dotnet = Path.Combine(buildDir, "dotnet.js"); - private readonly string runtime = Path.Combine(buildDir, "dotnet.runtime.js"); - private readonly string native = Path.Combine(buildDir, "dotnet.native.js"); - private readonly string dotnetGen = Path.Combine(buildDir, "dotnet.g.js"); - private readonly string runtimeGen = Path.Combine(buildDir, "dotnet.runtime.g.js"); - private readonly string nativeGen = Path.Combine(buildDir, "dotnet.native.g.js"); - - public void Patch () - { - if (thread) PatchThreading(); - if (embed) new InternalPatcher(dotnet, runtime, native).Patch(); - RemoveMaps(); - RemoveWasmNag(); - CopyInternals(); - } - - private void RemoveWasmNag () - { - // Removes "WebAssembly resource does not have the expected content type..." warning. - - File.WriteAllText(dotnet, new Regex("""(?:[$\w]+\.)*[$\w]+\(\s*(['"])WebAssembly resource does not have the expected content type \\?"application/wasm\\?", so falling back to slower ArrayBuffer instantiation\.\1\s*\)""", - RegexOptions.Compiled | RegexOptions.CultureInvariant) - .Replace(File.ReadAllText(dotnet, Encoding.UTF8), "true"), Encoding.UTF8); - } - - private void PatchThreading () - { - // Overprotective browser-only assert breaks unit testing: - // https://github.com/dotnet/runtime/issues/92853. - - File.WriteAllText(dotnet, File.ReadAllText(dotnet, Encoding.UTF8) - .Replace("&&Te(!1,\"This build of dotnet is multi-threaded, it doesn't support shell environments like V8 or NodeJS. See also https://aka.ms/dotnet-wasm-features\")", ""), Encoding.UTF8); - } - - private void RemoveMaps () - { - // Microsoft bundles .NET JavaScript sources pre-minified/uglified with source maps - // referencing upstream sources we don't publish with the package. - - var regex = new Regex(@"^\s*//# sourceMappingURL=.*?\.map\s*$\r?\n?", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline); - File.WriteAllText(dotnet, regex.Replace(File.ReadAllText(dotnet, Encoding.UTF8), ""), Encoding.UTF8); - File.WriteAllText(runtime, regex.Replace(File.ReadAllText(runtime, Encoding.UTF8), ""), Encoding.UTF8); - File.WriteAllText(native, regex.Replace(File.ReadAllText(native, Encoding.UTF8), ""), Encoding.UTF8); - } - - private void CopyInternals () - { - if (embed) - { - File.WriteAllText(dotnetGen, File.ReadAllText(dotnet, Encoding.UTF8), Encoding.UTF8); - File.WriteAllText(runtimeGen, File.ReadAllText(runtime, Encoding.UTF8), Encoding.UTF8); - File.WriteAllText(nativeGen, File.ReadAllText(native, Encoding.UTF8), Encoding.UTF8); - } - else - { - var mt = thread.ToString().ToLowerInvariant(); - var content = $"export const embedded = false;\nexport const mt = {mt};"; - File.WriteAllText(dotnetGen, content, Encoding.UTF8); - File.WriteAllText(runtimeGen, content, Encoding.UTF8); - File.WriteAllText(nativeGen, content, Encoding.UTF8); - } - } -} diff --git a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs index 911814a4..7f9cadbe 100644 --- a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish; -internal sealed class ResourceGenerator (string entryAssemblyName, bool embed, bool debug, bool g11n) +internal sealed class ResourceGenerator (string entryAssemblyName, bool debug, bool g11n) { private readonly List assemblies = []; private readonly List symbols = []; @@ -11,19 +11,19 @@ internal sealed class ResourceGenerator (string entryAssemblyName, bool embed, b public string Generate (string buildDir, string debugDir) { foreach (var path in Directory.GetFiles(buildDir, "*.wasm").Order()) - if (path.EndsWith("dotnet.native.wasm")) wasm = BuildBin(path); - else assemblies.Add(BuildBin(path)); + if (path.EndsWith("dotnet.native.wasm")) wasm = BuildResource(path); + else assemblies.Add(BuildResource(path)); if (g11n) { foreach (var path in Directory.GetFiles(buildDir, "*.dat").Order()) - icu.Add(BuildBin(path)); + icu.Add(BuildResource(path)); } if (debug) { foreach (var path in Directory.GetFiles(debugDir, "*.symbols").Order()) - symbols.Add(BuildBin(path)); + symbols.Add(BuildResource(path)); foreach (var path in Directory.GetFiles(debugDir, "*.pdb").Order()) - pdb.Add(BuildBin(path)); + pdb.Add(BuildResource(path)); } return $$""" @@ -46,12 +46,9 @@ public string Generate (string buildDir, string debugDir) """; } - private string BuildBin (string path) + private string BuildResource (string path) { var name = Path.GetFileName(path); - var content = embed ? ToBase64(File.ReadAllBytes(path)) : "undefined"; - return $$"""{ name: "{{name}}", content: {{content}} }"""; + return $$"""{ name: "{{name}}" }"""; } - - private string ToBase64 (byte[] bytes) => $"\"{Convert.ToBase64String(bytes)}\""; } diff --git a/src/cs/Bootsharp/Build/Bootsharp.props b/src/cs/Bootsharp/Build/Bootsharp.props index 3f671d84..65ba4a3d 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.props +++ b/src/cs/Bootsharp/Build/Bootsharp.props @@ -17,15 +17,11 @@ bootsharp - - true - - - + diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 26115627..5cdd7bc0 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -94,17 +94,13 @@ $(BootsharpPublishDirectory)/bin $(BootsharpPublishDirectory) $([System.String]::Equals('$(InvariantGlobalization)', 'false')) - $([System.String]::Equals('$(WasmEnableThreads)', 'true')) - false - --sourcemap - npx --yes rollup index.js -o index.mjs -f es -e process,module,fs/promises --output.inlineDynamicImports $(BsBundleMapsArg) - + - + - + - - - - + - + + - - - - - - - - - + + + + + + + @@ -168,12 +148,9 @@ - - - + + + diff --git a/src/cs/Bootsharp/Build/PackageTemplate.json b/src/cs/Bootsharp/Build/PackageTemplate.json index 7390f26e..ab38a3e5 100644 --- a/src/cs/Bootsharp/Build/PackageTemplate.json +++ b/src/cs/Bootsharp/Build/PackageTemplate.json @@ -2,5 +2,5 @@ "name": "%MODULE_NAME%", "type": "module", "main": "%MODULE_DIR%/index.mjs", - "types": "%TYPES_DIR%/index.d.ts" + "types": "%TYPES_DIR%/index.d.mts" } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index c6726447..d4354a7e 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.174 + 0.8.0-alpha.198 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/package.json b/src/js/package.json index d3c9a472..cf40f5d1 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,16 +1,16 @@ { "scripts": { - "compile-test": "sh scripts/compile-test.sh", - "test": "sh scripts/test.sh", - "cover": "sh scripts/cover.sh", - "build": "sh scripts/build.sh" + "build": "bash scripts/build.sh", + "compile-test": "bash scripts/compile-test.sh", + "test": "vitest run", + "cover": "vitest run --coverage --coverage.thresholds.100 --coverage.include=**/bootsharp/**/*.mjs" }, "devDependencies": { - "typescript": "5.8.2", - "@types/node": "22.13.14", - "@types/ws": "8.18.0", - "vitest": "3.0.9", - "@vitest/coverage-v8": "3.0.9", - "ws": "8.18.1" + "typescript": "6.0.3", + "@types/node": "25.6.2", + "@types/ws": "8.18.1", + "vitest": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "ws": "8.20.0" } } diff --git a/src/js/scripts/build.sh b/src/js/scripts/build.sh index b524c364..9a4c4ab2 100644 --- a/src/js/scripts/build.sh +++ b/src/js/scripts/build.sh @@ -1,4 +1,5 @@ rm -rf dist tsc --outDir dist --declaration -cp src/dotnet.g.d.ts dist/dotnet.g.d.ts -rm dist/*.g.js +mkdir -p dist/dotnet +cp src/dotnet/*.d.ts dist/dotnet/ +rm dist/generated/*.g.mjs diff --git a/src/js/scripts/compile-test.sh b/src/js/scripts/compile-test.sh index d7f2c7d7..3019a913 100644 --- a/src/js/scripts/compile-test.sh +++ b/src/js/scripts/compile-test.sh @@ -1,6 +1,4 @@ cd test/cs rm -rf Test/bin Test/obj Test.Library/bin Test.Library/obj dotnet restore --no-cache --force-evaluate -dotnet publish -p BootsharpName=embedded -p BootsharpEmbedBinaries=true -c Debug -rm -rf Test/bin/Debug Test/obj Test.Library/bin Test.Library/obj -dotnet publish -p BootsharpName=sideload -p BootsharpEmbedBinaries=false -c Debug +dotnet publish -c Debug diff --git a/src/js/scripts/cover.sh b/src/js/scripts/cover.sh deleted file mode 100644 index ff905c4a..00000000 --- a/src/js/scripts/cover.sh +++ /dev/null @@ -1,3 +0,0 @@ -node ./node_modules/vitest/vitest.mjs run \ - --coverage.enabled --coverage.thresholds.100 --coverage.include=**/sideload/*.mjs \ - --coverage.exclude=**/dotnet.* --coverage.allowExternal diff --git a/src/js/scripts/test.sh b/src/js/scripts/test.sh deleted file mode 100644 index 57337677..00000000 --- a/src/js/scripts/test.sh +++ /dev/null @@ -1 +0,0 @@ -node ./node_modules/vitest/vitest.mjs run diff --git a/src/js/src/bindings.g.ts b/src/js/src/bindings.g.ts deleted file mode 100644 index d0483422..00000000 --- a/src/js/src/bindings.g.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Autogenerated and resolved when building C# solution. -export default {}; diff --git a/src/js/src/boot.mts b/src/js/src/boot.mts new file mode 100644 index 00000000..8c5bc6b4 --- /dev/null +++ b/src/js/src/boot.mts @@ -0,0 +1,74 @@ +import { RuntimeConfig, RuntimeAPI, app } from "./dotnet/index.mjs"; +import { BootResources, fetchResources } from "./resources.mjs"; +import { buildConfig } from "./config.mjs"; +import { bindImports } from "./imports.mjs"; +import { bindExports } from "./exports.mjs"; +import { setRuntime } from "./runtime.mjs"; + +/** Lifecycle status of the runtime module. */ +export enum BootStatus { + /** Ready to boot. */ + Standby, + /** Async boot process is in progress. */ + Booting, + /** Booted and ready for interop. */ + Booted +} + +/** Configuration of the runtime boot process. */ +export type BootOptions = { + /** Resources required to boot the runtime. */ + readonly resources: BootResources; + /** Custom runtime configuration. */ + readonly config?: RuntimeConfig; + /** Customization hook for creating the runtime instance. */ + readonly create?: (config: RuntimeConfig) => Promise; + /** Customization hook for binding imported C# APIs. */ + readonly import?: (runtime: RuntimeAPI) => Promise; + /** Customization hook for binding exported C# APIs. */ + readonly export?: (runtime: RuntimeAPI) => Promise; + /** Customization hook for starting the runtime. */ + readonly run?: (runtime: RuntimeAPI) => Promise; +} + +let status = BootStatus.Standby; + +/** Returns current runtime module lifecycle state. */ +export function getStatus(): BootStatus { + return status; +} + +/** Initializes the runtime and binds C# APIs. + * @param opt Either URL to the boot resources root (eg, /bin) or a full configuration. + * @return Promise that resolves into the runtime instance when the initialization is finished. */ +export async function boot(opt: string | BootOptions): Promise { + if (status === BootStatus.Booted) throw Error("Failed to boot the C# runtime: already booted."); + if (status === BootStatus.Booting) throw Error("Failed to boot the C# runtime: already booting."); + status = BootStatus.Booting; + const options = typeof opt === "string" ? { resources: await fetchResources(opt) } : opt; + const runtime = await createRuntime(options); + status = BootStatus.Booted; + return runtime; +} + +/** Terminates the runtime and removes WASM module from memory. + * @param code Exit code; will use 0 (normal exit) by default. + * @param reason Exit reason description (optional). */ +export async function exit(code?: number, reason?: string): Promise { + /* v8 ignore start -- @preserve */ // Uncoverable, as exit terminates the host test process. + if (status !== BootStatus.Booted) throw Error("Failed to exit the C# runtime: not booted."); + try { app.exit(code ?? 0, reason); } + catch { } + finally { status = BootStatus.Standby; } + /* v8 ignore stop -- @preserve */ +} + +async function createRuntime(opt: BootOptions) { + const cfg = opt.config ?? buildConfig(opt.resources); + const runtime = await opt.create?.(cfg) || await app.dotnet.withConfig(cfg).create(); + setRuntime(runtime); + if (opt.import) await opt.import(runtime); else bindImports(runtime); + if (opt.run) await opt.run(runtime); else await runtime.runMain(cfg.mainAssemblyName!, []); + if (opt.export) await opt.export(runtime); else await bindExports(runtime, cfg.mainAssemblyName!); + return runtime; +} diff --git a/src/js/src/boot.ts b/src/js/src/boot.ts deleted file mode 100644 index 1c42286e..00000000 --- a/src/js/src/boot.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { RuntimeConfig, RuntimeAPI, getMain, ModuleAPI } from "./modules"; -import { BootResources, resources } from "./resources"; -import { buildConfig } from "./config"; -import { bindImports } from "./imports"; -import { bindExports } from "./exports"; -import { setRuntime } from "./runtime"; - -/** Lifecycle status of the runtime module. */ -export enum BootStatus { - /** Ready to boot. */ - Standby, - /** Async boot process is in progress. */ - Booting, - /** Booted and ready for interop. */ - Booted -} - -/** Boot process configuration. */ -export type BootOptions = { - /** Absolute path to the directory where boot resources are hosted (eg, /bin). */ - readonly root?: string; - /** Resources required to boot .NET runtime. */ - readonly resources?: BootResources; - /** .NET runtime configuration. */ - readonly config?: RuntimeConfig; - /** Creates .NET runtime instance. */ - readonly create?: (config: RuntimeConfig) => Promise; - /** Binds imported C# APIs. */ - readonly import?: (runtime: RuntimeAPI) => Promise; - /** Starts .NET runtime. */ - readonly run?: (runtime: RuntimeAPI) => Promise; - /** Binds exported C# APIs. */ - readonly export?: (runtime: RuntimeAPI) => Promise; -} - -let status = BootStatus.Standby; -let main: ModuleAPI | undefined; - -/** Returns current runtime module lifecycle state. */ -export function getStatus(): BootStatus { - return status; -} - -/** Initializes .NET runtime and binds C# APIs. - * @param options Specify to configure the boot process. - * @return Promise that resolves into .NET runtime instance. */ -export async function boot(options?: BootOptions): Promise { - if (status === BootStatus.Booted) throw Error("Failed to boot .NET runtime: already booted."); - if (status === BootStatus.Booting) throw Error("Failed to boot .NET runtime: already booting."); - status = BootStatus.Booting; - main = await getMain(options?.root); - const runtime = await createRuntime(main, options); - status = BootStatus.Booted; - return runtime; -} - -/** Terminates .NET runtime and removes WASM module from memory. - * @param code Exit code; will use 0 (normal exit) by default. - * @param reason Exit reason description (optional). */ -export async function exit(code?: number, reason?: string): Promise { - if (status !== BootStatus.Booted) throw Error("Failed to exit .NET runtime: not booted."); - try { main?.exit(code ?? 0, reason); } - catch { } - finally { status = BootStatus.Standby; } -} - -async function createRuntime(main: ModuleAPI, opt?: BootOptions) { - const cfg = opt?.config ?? await buildConfig(opt?.resources ?? resources, opt?.root); - const runtime = await opt?.create?.(cfg) || await main.dotnet.withConfig(cfg).create(); - setRuntime(runtime); - if (opt?.import) await opt.import(runtime); else bindImports(runtime); - if (opt?.run) await opt.run(runtime); else await runtime.runMain(cfg.mainAssemblyName!, []); - if (opt?.export) await opt.export(runtime); else await bindExports(runtime, cfg.mainAssemblyName!); - return runtime; -} diff --git a/src/js/src/config.mts b/src/js/src/config.mts new file mode 100644 index 00000000..4a23bbe7 --- /dev/null +++ b/src/js/src/config.mts @@ -0,0 +1,49 @@ +import type { RuntimeConfig, Asset, WasmAsset, AssemblyAsset, IcuAsset, PdbAsset, SymbolsAsset, ModuleAsset } from "./dotnet/index.mjs"; +import type { BinaryResource, BootResources } from "./resources.mjs"; +import * as nativeModule from "./dotnet/dotnet.native.js"; +import * as runtimeModule from "./dotnet/dotnet.runtime.js"; + +/** Builds .NET runtime configuration from the specified boot resources. */ +export function buildConfig(resources: BootResources): RuntimeConfig { + return { + resources: { + wasmNative: [resolveAsset(resources.wasm)], + jsModuleNative: [resolveModule("dotnet.native.js", nativeModule)], + jsModuleRuntime: [resolveModule("dotnet.runtime.js", runtimeModule)], + assembly: resources.assemblies.map(resolveAsset), + icu: resources.icu.map(resolveAsset), + wasmSymbols: resources.symbols.map(resolveSymbols), + pdb: resources.pdb.map(resolveAsset) + }, + mainAssemblyName: resources.entryAssemblyName, + globalizationMode: resolveGlobalizationMode(), + debugLevel: resources.symbols.length > 0 ? -1 : undefined + }; + + function resolveModule(name: string, exports: unknown): ModuleAsset { + return { name, moduleExports: exports }; + } + + function resolveAsset(res: BinaryResource): T { + return { name: res.name, virtualPath: res.name, buffer: res.content } as Asset as T; + } + + function resolveSymbols(res: BinaryResource): SymbolsAsset { + // Use 'resolveAsset()' once https://github.com/dotnet/runtime/pull/127087 is merged. + const txt = new TextDecoder("utf-8").decode(res.content); + return { + name: res.name, + pendingDownload: { + name: res.name, + url: res.name, + response: Promise.resolve(new Response(txt, { status: 200 })) + } + }; + } + + function resolveGlobalizationMode(): RuntimeConfig["globalizationMode"] { + if (resources.icu.length === 0) return "invariant" as never; + if (resources.icu.some(res => res.name === "icudt.dat")) return "all" as never; + return "sharded" as never; + } +} diff --git a/src/js/src/config.ts b/src/js/src/config.ts deleted file mode 100644 index 3410052e..00000000 --- a/src/js/src/config.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { RuntimeConfig, Asset, WasmAsset, ModuleAsset, AssemblyAsset, IcuAsset, PdbAsset, SymbolsAsset, getRuntime, getNative } from "./modules"; -import { BinaryResource, BootResources } from "./resources"; -import { decodeBase64 } from "./decoder"; - -/** Builds .NET runtime configuration. - * @param resources Resources required for runtime initialization. - * @param root When specified, assumes boot resources are side-loaded from the specified root. */ -export async function buildConfig(resources: BootResources, root?: string): Promise { - const embed = root == null; - const mt = !embed && (await import("./dotnet.g")).mt; - const [wasm, native, runtime, assemblies, icu, symbols, pdb] = await Promise.all([ - resolveAsset(resources.wasm), - resolveModule("dotnet.native.js", embed ? getNative : undefined), - resolveModule("dotnet.runtime.js", embed ? getRuntime : undefined), - Promise.all(resources.assemblies.map(resolveAsset)), - Promise.all(resources.icu.map(resolveAsset)), - Promise.all(resources.symbols.map(resolveSymbols)), - Promise.all(resources.pdb.map(resolveAsset)) - ]); - return { - resources: { - wasmNative: [wasm], - jsModuleNative: [native], - jsModuleRuntime: [runtime], - jsModuleWorker: mt ? [await resolveModule("dotnet.native.worker.mjs")] : undefined, - assembly: assemblies, - wasmSymbols: symbols, - pdb: pdb, - icu: icu - }, - mainAssemblyName: resources.entryAssemblyName, - globalizationMode: resolveGlobalizationMode(), - debugLevel: resources.symbols.length > 0 ? -1 : undefined - }; - - function resolveGlobalizationMode(): RuntimeConfig["globalizationMode"] { - if (resources.icu.length === 0) return "invariant"; - if (resources.icu.some(res => res.name === "icudt.dat")) return "all"; - return "sharded"; - } - - async function resolveModule(name: string, embed?: () => Promise): Promise { - return { - name, - moduleExports: embed ? await embed() : undefined - }; - } - - async function resolveAsset(res: BinaryResource): Promise { - return { - name: res.name, - virtualPath: res.name, - buffer: await resolveBuffer(res) - }; - } - - async function resolveSymbols(res: BinaryResource): Promise { - // Use 'resolveAsset()' once https://github.com/dotnet/runtime/pull/127087 is merged. - const txt = new TextDecoder("utf-8").decode(await resolveBuffer(res)); - return { - name: res.name, - pendingDownload: { - name: res.name, - url: res.name, - response: Promise.resolve(new Response(txt, { status: 200 })) - } - }; - } - - async function resolveBuffer(res: BinaryResource): Promise { - if (typeof res.content === "string") return decodeBase64(res.content); - if (res.content !== undefined) return res.content.buffer; - if (!embed) return fetchBuffer(res); - throw Error(`Failed to resolve '${res.name}' boot resource.`); - } - - async function fetchBuffer(res: BinaryResource): Promise { - const path = `${root}/${res.name}`; - if (typeof window === "object") - return (await fetch(path)).arrayBuffer(); - if (typeof process === "object") { - const { readFile } = await import(/*@vite-ignore*//*webpackIgnore:true*/"fs/promises"); - const bin = await readFile(path); - return bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength); - } - throw Error(`Failed to fetch '${path}' boot resource: unsupported runtime.`); - } -} diff --git a/src/js/src/decoder.ts b/src/js/src/decoder.ts deleted file mode 100644 index f90c8d33..00000000 --- a/src/js/src/decoder.ts +++ /dev/null @@ -1,56 +0,0 @@ -const lookup = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 62, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); - -export function decodeBase64(source: string): ArrayBuffer { - if (typeof window === "object") return decodeWithBrowser(source); - if (typeof process === "object") return decodeWithNode(source); - return decodeNaive(source); -} - -function decodeWithBrowser(source: string): ArrayBuffer { - const binaryString = window.atob(source); - const length = binaryString.length; - const buffer = new ArrayBuffer(length); - const uint8Array = new Uint8Array(buffer); - for (let i = 0; i < length; i++) - uint8Array[i] = binaryString.charCodeAt(i); - return buffer; -} - -function decodeWithNode(source: string): ArrayBuffer { - const buffer = Buffer.from(source, "base64"); - return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); -} - -function decodeNaive(source: string): ArrayBuffer { - const srcLen = source.length; - const padLen = (source[srcLen - 2] === "=" ? 2 : (source[srcLen - 1] === "=" ? 1 : 0)); - const outLen = ((srcLen - padLen) * 3) >> 2; - const buffer = new Uint8Array(outLen); - - let tmp; - let byteIndex = 0; - - for (let i = 0, baseLen = srcLen - padLen; i < baseLen; i += 4) { - tmp = (lookup[source.charCodeAt(i)] << 18) - | (lookup[source.charCodeAt(i + 1)] << 12) - | (lookup[source.charCodeAt(i + 2)] << 6) - | (lookup[source.charCodeAt(i + 3)]); - buffer[byteIndex++] = (tmp >> 16) & 0xFF; - buffer[byteIndex++] = (tmp >> 8) & 0xFF; - buffer[byteIndex++] = tmp & 0xFF; - } - - if (padLen === 1) { - tmp = (lookup[source.charCodeAt(srcLen - 4)] << 18) - | (lookup[source.charCodeAt(srcLen - 3)] << 12) - | (lookup[source.charCodeAt(srcLen - 2)] << 6); - buffer[byteIndex++] = (tmp >> 16) & 0xFF; - buffer[byteIndex++] = (tmp >> 8) & 0xFF; - } else if (padLen === 2) { - tmp = (lookup[source.charCodeAt(srcLen - 4)] << 18) - | (lookup[source.charCodeAt(srcLen - 3)] << 12); - buffer[byteIndex++] = (tmp >> 16) & 0xFF; - } - - return buffer.buffer; -} diff --git a/src/js/src/dotnet.native.g.d.ts b/src/js/src/dotnet.native.g.d.ts deleted file mode 100644 index 47633c9b..00000000 --- a/src/js/src/dotnet.native.g.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Resolved when building C# solution. -export const embedded = false; diff --git a/src/js/src/dotnet.runtime.g.d.ts b/src/js/src/dotnet.runtime.g.d.ts deleted file mode 100644 index 47633c9b..00000000 --- a/src/js/src/dotnet.runtime.g.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Resolved when building C# solution. -export const embedded = false; diff --git a/src/js/src/dotnet/dotnet.d.ts b/src/js/src/dotnet/dotnet.d.ts new file mode 100644 index 00000000..f7dc9b72 --- /dev/null +++ b/src/js/src/dotnet/dotnet.d.ts @@ -0,0 +1,2 @@ +declare const dotnet: unknown; +export default dotnet; diff --git a/src/js/src/dotnet.g.d.ts b/src/js/src/dotnet/dotnet.g.d.ts similarity index 99% rename from src/js/src/dotnet.g.d.ts rename to src/js/src/dotnet/dotnet.g.d.ts index 2f8aeb3a..058f4887 100644 --- a/src/js/src/dotnet.g.d.ts +++ b/src/js/src/dotnet/dotnet.g.d.ts @@ -1,7 +1,3 @@ -// Resolved when building C# solution. -export const embedded = false; -export const mt = false; - // Types: https://github.com/dotnet/runtime/blob/release/10.0/src/mono/browser/runtime/dotnet.d.ts declare interface NativePointer { diff --git a/src/js/src/dotnet/dotnet.native.d.ts b/src/js/src/dotnet/dotnet.native.d.ts new file mode 100644 index 00000000..e402ffc5 --- /dev/null +++ b/src/js/src/dotnet/dotnet.native.d.ts @@ -0,0 +1,2 @@ +declare const native: unknown; +export default native; diff --git a/src/js/src/dotnet/dotnet.runtime.d.ts b/src/js/src/dotnet/dotnet.runtime.d.ts new file mode 100644 index 00000000..ef74f13d --- /dev/null +++ b/src/js/src/dotnet/dotnet.runtime.d.ts @@ -0,0 +1,2 @@ +declare const runtime: unknown; +export default runtime; diff --git a/src/js/src/dotnet/index.mts b/src/js/src/dotnet/index.mts new file mode 100644 index 00000000..63a606da --- /dev/null +++ b/src/js/src/dotnet/index.mts @@ -0,0 +1,16 @@ +// Modules under the 'src/dotnet' folder are copied when building the C# solution. + +import type { ModuleAPI, MonoConfig } from "./dotnet.g.d.ts"; +import * as dotnet from "./dotnet.js"; + +export type * from "./dotnet.g.d.ts"; +export type RuntimeConfig = MonoConfig; +export type RuntimeResources = NonNullable; +export type WasmAsset = NonNullable[number]; +export type ModuleAsset = NonNullable[number]; +export type AssemblyAsset = NonNullable[number]; +export type IcuAsset = NonNullable[number]; +export type PdbAsset = NonNullable[number]; +export type SymbolsAsset = NonNullable[number]; + +export const app = dotnet as unknown as ModuleAPI; diff --git a/src/js/src/event.ts b/src/js/src/event.mts similarity index 100% rename from src/js/src/event.ts rename to src/js/src/event.mts diff --git a/src/js/src/exports.ts b/src/js/src/exports.mts similarity index 86% rename from src/js/src/exports.ts rename to src/js/src/exports.mts index 90524187..66368879 100644 --- a/src/js/src/exports.ts +++ b/src/js/src/exports.mts @@ -1,4 +1,4 @@ -import type { RuntimeAPI } from "./modules"; +import type { RuntimeAPI } from "./dotnet/index.mjs"; export let exports: Record; diff --git a/src/js/src/generated/bindings.g.mts b/src/js/src/generated/bindings.g.mts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/src/js/src/generated/bindings.g.mts @@ -0,0 +1 @@ +export default {}; diff --git a/src/js/src/generated/resources.g.mts b/src/js/src/generated/resources.g.mts new file mode 100644 index 00000000..82fc6873 --- /dev/null +++ b/src/js/src/generated/resources.g.mts @@ -0,0 +1,3 @@ +import { BootResources } from "../resources.mjs"; + +export default {} as BootResources; diff --git a/src/js/src/imports.mts b/src/js/src/imports.mts new file mode 100644 index 00000000..2c6b37ea --- /dev/null +++ b/src/js/src/imports.mts @@ -0,0 +1,10 @@ +import * as generated from "./generated/bindings.g.mjs"; +import { instances } from "./instances.mjs"; +import type { RuntimeAPI } from "./dotnet/index.mjs"; + +export function bindImports(runtime: RuntimeAPI) { + runtime.setModuleImports("Bootsharp", { + ...generated, + instances + }); +} diff --git a/src/js/src/imports.ts b/src/js/src/imports.ts deleted file mode 100644 index 0f44ca48..00000000 --- a/src/js/src/imports.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as bindings from "./bindings.g"; -import { instances } from "./instances"; -import type { RuntimeAPI } from "./modules"; - -export function bindImports(runtime: RuntimeAPI) { - runtime.setModuleImports("Bootsharp", { - ...bindings, - instances - }); -} diff --git a/src/js/src/index.mts b/src/js/src/index.mts new file mode 100644 index 00000000..aa563bd4 --- /dev/null +++ b/src/js/src/index.mts @@ -0,0 +1,17 @@ +import { boot, exit, getStatus, BootStatus } from "./boot.mjs"; +import { resources } from "./resources.mjs"; +import { app } from "./dotnet/index.mjs"; + +export default { + boot, + exit, + getStatus, + BootStatus, + resources, + dotnet: app.dotnet +}; + +export * from "./event.mjs"; +export * from "./generated/bindings.g.mjs"; +export type { BootOptions } from "./boot.mjs"; +export type { BootResources, BinaryResource } from "./resources.mjs"; diff --git a/src/js/src/index.ts b/src/js/src/index.ts deleted file mode 100644 index ec6a98c8..00000000 --- a/src/js/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { boot, exit, getStatus, BootStatus } from "./boot"; -import { getMain, getNative, getRuntime } from "./modules"; -import { resources } from "./resources"; -import { buildConfig } from "./config"; - -export default { - boot, - exit, - getStatus, - BootStatus, - resources, - /** .NET internal modules and associated utilities. */ - dotnet: { getMain, getNative, getRuntime, buildConfig } -}; - -export * from "./event"; -export * from "./bindings.g"; -export type { BootOptions } from "./boot"; -export type { BootResources, BinaryResource } from "./resources"; diff --git a/src/js/src/instances.ts b/src/js/src/instances.mts similarity index 95% rename from src/js/src/instances.ts rename to src/js/src/instances.mts index d04a7353..6c808070 100644 --- a/src/js/src/instances.ts +++ b/src/js/src/instances.mts @@ -1,4 +1,4 @@ -import { exports } from "./exports"; +import { exports } from "./exports.mjs"; const exportedFinalizer = new FinalizationRegistry(finalizeExported); const exportedById = new Map>(); @@ -49,6 +49,6 @@ export const instances = { /* v8 ignore start -- @preserve */ // Uncoverable, as finalization in Node is not controllable. function finalizeExported(id: number) { exportedById.delete(id); - (<{ disposeExported: (id: number) => void }>exports).disposeExported(id); + (exports as { disposeExported: (id: number) => void }).disposeExported(id); } /* v8 ignore stop -- @preserve */ diff --git a/src/js/src/modules.ts b/src/js/src/modules.ts deleted file mode 100644 index 1bfaeb18..00000000 --- a/src/js/src/modules.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ModuleAPI, MonoConfig } from "./dotnet.g.d.ts"; -export type { Asset } from "./dotnet.g.d.ts"; - -export type * from "./dotnet.g.d.ts"; -export type RuntimeConfig = MonoConfig; -export type RuntimeResources = NonNullable; -export type WasmAsset = NonNullable[number]; -export type ModuleAsset = NonNullable[number]; -export type AssemblyAsset = NonNullable[number]; -export type IcuAsset = NonNullable[number]; -export type PdbAsset = NonNullable[number]; -export type SymbolsAsset = NonNullable[number]; - -/** Fetches the main dotnet module (dotnet.js). */ -export async function getMain(root?: string): Promise { - if (root == null) return await import("./dotnet.g"); - return await import(/*@vite-ignore*//*webpackIgnore:true*/`${root}/dotnet.js`); -} - -/** Fetches dotnet native module (dotnet.native.js). */ -export async function getNative(root?: string): Promise { - if (root == null) return await import("./dotnet.native.g"); - return await import(/*@vite-ignore*//*webpackIgnore:true*/`${root}/dotnet.native.js`); -} - -/** Fetches dotnet runtime module (dotnet.runtime.js). */ -export async function getRuntime(root?: string): Promise { - if (root == null) return await import("./dotnet.runtime.g"); - return await import(/*@vite-ignore*//*webpackIgnore:true*/`${root}/dotnet.runtime.js`); -} diff --git a/src/js/src/resources.g.ts b/src/js/src/resources.g.ts deleted file mode 100644 index b3cf9a4b..00000000 --- a/src/js/src/resources.g.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { BootResources } from "./resources"; - -// Autogenerated and resolved when building C# solution. -export default {} as BootResources; diff --git a/src/js/src/resources.mts b/src/js/src/resources.mts new file mode 100644 index 00000000..6cc1ccfb --- /dev/null +++ b/src/js/src/resources.mts @@ -0,0 +1,45 @@ +import generated from "./generated/resources.g.mjs"; + +/** Resources required to boot .NET runtime. */ +export type BootResources = { + /** Compiled .NET WASM runtime module. */ + readonly wasm: BinaryResource; + /** Compiled .NET assemblies. */ + readonly assemblies: BinaryResource[]; + /** Globalization data. */ + readonly icu: BinaryResource[]; + /** WASM debug symbols. */ + readonly symbols: BinaryResource[]; + /** PDB debug artifacts. */ + readonly pdb: BinaryResource[]; + /** Name of the entry (main) assembly, with .dll extension. */ + readonly entryAssemblyName: string; +} + +/** Boot resource with binary content. */ +export type BinaryResource = { + /** Name of the binary file, including extension. */ + readonly name: string; + /** Binary content of the file. */ + readonly content: ArrayBuffer; +} + +/** Resources required to boot .NET runtime. */ +export const resources: BootResources = generated; + +/** Fetches required boot resources from the specified root URL. */ +export async function fetchResources(root: string): Promise { + const [wasm, assemblies, icu, symbols, pdb] = await Promise.all([ + fetchResource(resources.wasm), + Promise.all(resources.assemblies.map(fetchResource)), + Promise.all(resources.icu.map(fetchResource)), + Promise.all(resources.symbols.map(fetchResource)), + Promise.all(resources.pdb.map(fetchResource)) + ]); + return { wasm, assemblies, icu, symbols, pdb, entryAssemblyName: resources.entryAssemblyName }; + + async function fetchResource(r: BinaryResource): Promise { + const content = await (await fetch(`${root}/${r.name}`)).arrayBuffer(); + return { name: r.name, content }; + } +} diff --git a/src/js/src/resources.ts b/src/js/src/resources.ts deleted file mode 100644 index 8ec433f9..00000000 --- a/src/js/src/resources.ts +++ /dev/null @@ -1,28 +0,0 @@ -import generated from "./resources.g"; - -/** Resources required to boot .NET runtime. */ -export type BootResources = { - /** Compiled .NET WASM runtime module. */ - readonly wasm: BinaryResource; - /** Compiled .NET assemblies. */ - readonly assemblies: BinaryResource[]; - /** Globalization data. */ - readonly icu: BinaryResource[]; - /** WASM debug symbols. */ - readonly symbols: BinaryResource[]; - /** PDB debug artifacts. */ - readonly pdb: BinaryResource[]; - /** Name of the entry (main) assembly, with .dll extension. */ - readonly entryAssemblyName: string; -} - -/** Boot resource with binary content. */ -export type BinaryResource = { - /** Name of the binary file, including extension. */ - readonly name: string; - /** Binary or base64-encoded content of the file; undefined when embedding disabled. */ - readonly content?: Uint8Array | string; -} - -/** Resources required to boot .NET runtime. */ -export const resources: BootResources = generated; diff --git a/src/js/src/runtime.ts b/src/js/src/runtime.mts similarity index 88% rename from src/js/src/runtime.ts rename to src/js/src/runtime.mts index 1bfa8f2f..fd4fd36b 100644 --- a/src/js/src/runtime.ts +++ b/src/js/src/runtime.mts @@ -1,4 +1,4 @@ -import type { RuntimeAPI } from "./modules"; +import type { RuntimeAPI } from "./dotnet/index.mjs"; let runtime: RuntimeAPI; diff --git a/src/js/src/serialization.ts b/src/js/src/serialization.mts similarity index 97% rename from src/js/src/serialization.ts rename to src/js/src/serialization.mts index b7b1ba1c..9df383eb 100644 --- a/src/js/src/serialization.ts +++ b/src/js/src/serialization.mts @@ -1,4 +1,4 @@ -import { malloc, free, getHeap } from "./runtime"; +import { malloc, free, getHeap } from "./runtime.mjs"; export type Binary = { write: Write; @@ -102,15 +102,15 @@ export const types = { (writer, value: Date) => writer.writeInt64((BigInt(value.getTime()) * 10000n) + dotnetEpochTicks), reader => new Date(Number((reader.readInt64() - dotnetEpochTicks) / 10000n))), - Nullable: (inner: Binary): Binary => binary( + Nullable: (inner: Binary): Binary => binary( (writer, value) => writeNullable(writer, value, inner), reader => readNullable(reader, inner)), - Array: (element: Binary): Binary | null | undefined> => binary( + Array: (element: Binary): Binary | null | undefined> => binary( (writer, value) => writeArray(writer, value, element), reader => readArray(reader, element)), - List: (element: Binary): Binary | null | undefined> => binary( + List: (element: Binary): Binary | null | undefined> => binary( (writer, value) => writeList(writer, value, element), reader => readList(reader, element)), diff --git a/src/js/test/cs.ts b/src/js/test/cs.ts index ffba6921..1c7b7377 100644 --- a/src/js/test/cs.ts +++ b/src/js/test/cs.ts @@ -1,82 +1,44 @@ -import emb, { Test as EmbTest } from "./cs/Test/bin/embedded"; -import sid, { Test as SidTest } from "./cs/Test/bin/sideload"; import assert from "node:assert"; -import { resolve, parse, basename } from "node:path"; -import { readdirSync, readFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { readFileSync, existsSync } from "node:fs"; +import type { BootResources } from "./cs/Test/bin/bootsharp"; +import bootsharp, { Test } from "./cs/Test/bin/bootsharp"; -export const embedded = emb; -export const sideload = sid; -export const EmbeddedTest = EmbTest; -export const SideloadTest = SidTest; -export const root = "./test/cs/Test/bin/sideload/bin"; +export { bootsharp, Test }; +export * from "./cs/Test/bin/bootsharp"; -export * from "./cs/Test/bin/sideload"; +export const resources: BootResources = loadResources(); -assertPathExists("test/cs/Test/bin/embedded/index.mjs"); -assertPathExists("test/cs/Test/bin/sideload/index.mjs"); - -export const bins = { - wasm: loadWasmBinary(), - assemblies: loadAssemblies(), - entryAssemblyName: "Test.dll" -}; - -export async function bootEmbedded() { - EmbeddedTest.Program.onMainInvoked = () => {}; - await embedded.boot({}); -} - -export async function bootSideload() { - SideloadTest.Program.onMainInvoked = () => {}; - await sideload.boot({ root }); +export async function bootRuntime() { + Test.Program.onMainInvoked = () => {}; + await bootsharp.boot({ resources }); } export function getDeclarations() { - const file = resolve("test/cs/Test/bin/embedded/types/bindings.g.d.ts"); - assertPathExists(file); + const file = resolvePath("test/cs/Test/bin/bootsharp/types/generated/bindings.g.d.mts"); return readFileSync(file).toString(); } -// Just casting to triggers codefactor. -export function any(obj: unknown): Record { - return >obj; -} - -export function to(obj: unknown): Record { - return >obj; -} - -function loadWasmBinary() { - const file = resolve("test/cs/Test/bin/sideload/bin/dotnet.native.wasm"); - assertPathExists(file); - return readFileSync(file); -} - -function loadAssemblies() { - const assemblies = []; - for (const assemblyPath of findAssemblies()) - assemblies.push(loadAssembly(assemblyPath)); - return assemblies; -} - -function findAssemblies() { - const assemblyPaths = []; - const dirPath = resolve("test/cs/Test/bin/sideload/bin"); - assertPathExists(dirPath); - for (const fileName of readdirSync(dirPath)) - if (!fileName.endsWith("dotnet.native.wasm") && fileName.endsWith(".wasm")) - assemblyPaths.push(`${dirPath}/${fileName}`); - return assemblyPaths; -} - -function loadAssembly(assemblyPath: string) { +function loadResources(): BootResources { return { - name: parse(assemblyPath).base, - content: readFileSync(assemblyPath) + wasm: load(bootsharp.resources.wasm.name), + assemblies: bootsharp.resources.assemblies.map(r => load(r.name)), + icu: bootsharp.resources.icu.map(r => load(r.name)), + symbols: bootsharp.resources.symbols.map(r => load(r.name)), + pdb: bootsharp.resources.pdb.map(r => load(r.name)), + entryAssemblyName: bootsharp.resources.entryAssemblyName }; } -function assertPathExists(pathToCheck: string) { - const name = basename(pathToCheck); - assert(existsSync(pathToCheck), `Missing test project artifact: '${name}'. Run 'scripts/compile-test.sh'.`); +function load(name: string) { + const path = resolvePath(`test/cs/Test/bin/bootsharp/bin/${name}`); + const bytes = readFileSync(path); + const content = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; + return { name, content }; +} + +function resolvePath(path: string) { + const resolved = resolve(path); + assert(existsSync(resolved), `Missing test project artifact: '${path}'. Run 'scripts/compile-test.sh'.`); + return resolved; } diff --git a/src/js/test/cs/Test/Platform.cs b/src/js/test/cs/Test/Platform.cs index a0870ec4..e758972f 100644 --- a/src/js/test/cs/Test/Platform.cs +++ b/src/js/test/cs/Test/Platform.cs @@ -31,6 +31,13 @@ public static string FormatDate (string culture, int month, int day, string form [Export] public static void ThrowCS (string message) => throw new Exception(message); + [Export] + public static async Task ThrowCSAsync (string message) + { + await Task.Delay(1); + throw new Exception(message); + } + [Import] public static partial void ThrowJS (); diff --git a/src/js/test/cs/Test/Test.csproj b/src/js/test/cs/Test/Test.csproj index 609ec9bf..a402c60b 100644 --- a/src/js/test/cs/Test/Test.csproj +++ b/src/js/test/cs/Test/Test.csproj @@ -4,14 +4,13 @@ net10.0 enable browser-wasm + true true bin/codegen false - npx --yes rollup index.js -d ./ -f es -e process,module,fs/promises --output.preserveModules --entryFileNames [name].mjs --sourcemap false true true - true CS1591 diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 4c81cffc..80cafabe 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -1,204 +1,120 @@ -import { describe, expect, it, vi } from "vitest"; import { resolve } from "node:path"; -import { Buffer } from "node:buffer"; -import type { BootOptions } from "../cs/Test/bin/sideload"; +import { readFileSync } from "node:fs"; +import { describe, expect, it, vi } from "vitest"; +import type { BootOptions } from "../cs/Test/bin/bootsharp"; async function setup() { - // dotnet merges with the host node process, so it's not possible - // to exit w/o killing the test process (which is bound to test file); - // this is a workaround to simulate clean environment in each test vi.resetModules(); const cs = await import("../cs"); - cs.SideloadTest.Program.onMainInvoked = vi.fn(); - cs.EmbeddedTest.Program.onMainInvoked = vi.fn(); - return { - ...cs, - side: { bootsharp: cs.sideload, Test: cs.SideloadTest }, - embed: { bootsharp: cs.embedded, Test: cs.EmbeddedTest } - }; + cs.Test.Program.onMainInvoked = vi.fn(); + return cs; } describe("boot", () => { - it("uses embedded modules when root is not specified", async () => { - const { side: { bootsharp } } = await setup(); - expect((await bootsharp.dotnet.getMain()).embedded).toStrictEqual(false); - expect((await bootsharp.dotnet.getNative()).embedded).toStrictEqual(false); - expect((await bootsharp.dotnet.getRuntime()).embedded).toStrictEqual(false); + it("is standby by default", async () => { + const { bootsharp } = await setup(); + expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); + }); + it("throws when exit invoked while not booted", async () => { + const { bootsharp } = await setup(); + await expect(bootsharp.exit).rejects.toThrow(/not booted/); }); - it("uses sideload modules when root is specified", async () => { - const { side: { bootsharp }, root } = await setup(); - expect((await bootsharp.dotnet.getMain(root)).embedded).toBeUndefined(); - expect((await bootsharp.dotnet.getNative(root)).embedded).toBeUndefined(); - expect((await bootsharp.dotnet.getRuntime(root)).embedded).toBeUndefined(); + it("transitions to booting and then to booted", async () => { + const { bootsharp, resources } = await setup(); + const promise = bootsharp.boot({ resources }); + expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booting); + await promise; + expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); }); - it("defines module exports when root is not specified", async () => { - const { embed: { bootsharp } } = await setup(); - const config = await bootsharp.dotnet.buildConfig(bootsharp.resources); - expect(config.resources!.jsModuleNative[0].moduleExports).toBeDefined(); - expect(config.resources!.jsModuleRuntime[0].moduleExports).toBeDefined(); + it("throws when boot invoked while booted", async () => { + const { bootsharp, resources } = await setup(); + await bootsharp.boot({ resources }); + await expect(bootsharp.boot({ resources })).rejects.toThrow(/already booted/); + }); + it("throws when boot invoked while booting", async () => { + const { bootsharp, resources } = await setup(); + const boot = bootsharp.boot({ resources }); + await expect(bootsharp.boot({ resources })).rejects.toThrow(/already booting/); + await boot; + }); + it("invokes program main on boot", async () => { + const { bootsharp, resources, Test } = await setup(); + await bootsharp.boot({ resources }); + expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); it("enables debugging when debugging resources are present", async () => { - const { side: { bootsharp }, root } = await setup(); - const config = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); + const { bootsharp, resources } = await setup(); + const config = (await bootsharp.boot({ resources })).getConfig(); expect(config.debugLevel).not.toBeUndefined(); }); it("doesn't enable debugging when debug are absent", async () => { - const { side: { bootsharp }, root } = await setup(); - const resources = { ...bootsharp.resources, symbols: [], pdb: [] }; - const config = await bootsharp.dotnet.buildConfig(resources, root); + const { bootsharp, resources } = await setup(); + const config = (await bootsharp.boot({ resources: { ...resources, symbols: [], pdb: [] } })).getConfig(); expect(config.debugLevel).toBeUndefined(); }); it("uses full globalization mode when full ICU resource is present", async () => { - const { side: { bootsharp }, root } = await setup(); - const config = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); + const { bootsharp, resources } = await setup(); + const config = (await bootsharp.boot({ resources })).getConfig(); expect(config.globalizationMode).toStrictEqual("all"); }); it("uses sharded globalization mode when sharded ICU resource is present", async () => { - const { side: { bootsharp }, root } = await setup(); - const resources = { ...bootsharp.resources, icu: [{ name: "icudt_CJK.dat", content: new Uint8Array() }] }; - const config = await bootsharp.dotnet.buildConfig(resources, root); + const { bootsharp, resources } = await setup(); + const load = (name: string) => { + const bytes = readFileSync(resolve(`test/cs/Test/bin/Debug/net10.0/browser-wasm/${name}`)); + return { name, content: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }; + }; + const icu = ["icudt_CJK.dat", "icudt_EFIGS.dat", "icudt_no_CJK.dat"].map(load); + const config = (await bootsharp.boot({ resources: { ...resources, icu } })).getConfig(); expect(config.globalizationMode).toStrictEqual("sharded"); }); it("disables globalization when ICU resources are absent", async () => { - const { side: { bootsharp }, root } = await setup(); - const resources = { ...bootsharp.resources, icu: [] }; - const config = await bootsharp.dotnet.buildConfig(resources, root); + const { bootsharp, resources } = await setup(); + const config = (await bootsharp.boot({ resources: { ...resources, icu: [] } })).getConfig(); expect(config.globalizationMode).toStrictEqual("invariant"); }); - it("throws when missing boot resource", async () => { - const { side: { bootsharp } } = await setup(); - await expect(bootsharp.dotnet.buildConfig(bootsharp.resources)) - .rejects.toThrowError(/Failed to resolve '.+' boot resource/); - }); - it("throws when attempting to fetch boot resource in sandbox", async () => { - const { side: { bootsharp }, root, any } = await setup(); - const win = any(global).window; - const proc = any(global).process; - any(global).window = undefined; - any(global).process = undefined; - await expect(bootsharp.dotnet.buildConfig(bootsharp.resources, root)) - .rejects.toThrowError(/Failed to fetch '.+' boot resource: unsupported runtime/); - any(global).window = win; - any(global).process = proc; - }); - it("uses fetch when fetching boot resource in browser", async () => { - const { side: { bootsharp }, root, any, bins } = await setup(); - const win = any(global).window; + it("fetches resources when root is specified", async () => { + const { bootsharp, resources, Test } = await setup(); + const bin = [resources.wasm, ...resources.assemblies, ...resources.icu, ...resources.symbols, ...resources.pdb]; + const fetchSpy = vi.fn(url => { + const name = url.substring(url.lastIndexOf("/") + 1); + const content = bin.find(r => r.name === name)!.content; + return Promise.resolve({ arrayBuffer: () => Promise.resolve(content) }); + }); const fetch = global.fetch; - any(global).window = {}; - any(global).fetch = vi.fn(() => ({ arrayBuffer: () => bins.wasm })); - await bootsharp.dotnet.buildConfig(bootsharp.resources, root); - expect(global.fetch).toHaveBeenCalled(); - any(global).window = win; - any(global).fetch = fetch; - }); - it("can boot with embedded resources", async () => { - const { embed: { bootsharp, Test } } = await setup(); - await bootsharp.boot({}); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); - }); - it("can boot while streaming resources from root", async () => { - const { side: { bootsharp }, root, Test } = await setup(); - await bootsharp.boot({ root }); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); - }); - it("can boot with resources content pre-assigned", async () => { - const { side: { bootsharp }, Test, root, bins, any } = await setup(); - const resources = { ...bootsharp.resources }; - any(resources.wasm).content = bins.wasm; - for (const asm of resources.assemblies) - any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content; - await bootsharp.boot({ resources, root }); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); - }); - it("can boot with base64 content", async () => { - const { side: { bootsharp }, Test, root, bins, any } = await setup(); - const resources = { ...bootsharp.resources }; - any(resources.wasm).content = bins.wasm.toString("base64"); - for (const asm of resources.assemblies) - any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); - await bootsharp.boot({ resources, root }); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); - }); - it("uses atob when window is defined in global", async () => { - const { bins, any } = await setup(); - const win = any(global).window; - const proc = any(global).process; - any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; - any(global).process = undefined; - // @ts-expect-error: white-boxing because mocking window breaks other stuff in boot - const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); - try { decodeBase64(bins.wasm.toString("base64")); } - catch {} - expect(global.window.atob).toHaveBeenCalled(); - any(global).window = win; - any(global).process = proc; - }); - it("uses naive decoder when neither window nor process are defined", async () => { - const { bins, any } = await setup(); - const win = any(global).window; - const proc = any(global).process; - any(global).window = undefined; - any(global).process = undefined; - // @ts-expect-error: white-boxing because mocking proc and window breaks other stuff in boot - const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); - for (const ass of bins.assemblies) - expect(decodeBase64(ass.content.toString("base64")).byteLength) - .toStrictEqual(ass.content.length); - any(global).window = win; - any(global).process = proc; - }); - it("throws when boot invoked while booted", async () => { - const { side: { bootsharp }, root } = await setup(); - await bootsharp.boot({ root }); - await expect(bootsharp.boot).rejects.toThrow(/already booted/); - }); - it("throws when boot invoked while booting", async () => { - const { side: { bootsharp }, root } = await setup(); - const boot = bootsharp.boot({ root }); - await expect(bootsharp.boot).rejects.toThrow(/already booting/); - await boot; - }); - it("throws when exit invoked while not booted", async () => { - const { side: { bootsharp } } = await setup(); - await expect(bootsharp.exit).rejects.toThrow(/not booted/); - }); - it("can exit when booted", async () => { - const { side: { bootsharp }, root } = await setup(); - await bootsharp.boot({ root }); - await bootsharp.exit(); - expect(bootsharp.getStatus()).toStrictEqual(0); + global.fetch = fetchSpy; + try { await bootsharp.boot("/bin"); } + finally { global.fetch = fetch; } + expect(Test.Program.onMainInvoked).toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalledWith("/bin/dotnet.native.wasm"); + expect(fetchSpy).toHaveBeenCalledWith("/bin/Bootsharp.Common.wasm"); }); it("respects boot customs", async () => { - const { side: { bootsharp }, bins, root } = await setup(); + const { bootsharp, resources } = await setup(); const customs: BootOptions = { config: { - mainAssemblyName: bins.entryAssemblyName, + mainAssemblyName: resources.entryAssemblyName, resources: { jsModuleRuntime: [{ name: "dotnet.runtime.js", - resolvedUrl: resolve("test/cs/Test/bin/sideload/bin/dotnet.runtime.js") + resolvedUrl: resolve("test/cs/Test/bin/bootsharp/dotnet/dotnet.runtime.js") }], jsModuleNative: [{ name: "dotnet.native.js", - resolvedUrl: resolve("test/cs/Test/bin/sideload/bin/dotnet.native.js") + resolvedUrl: resolve("test/cs/Test/bin/bootsharp/dotnet/dotnet.native.js") }], wasmNative: [{ name: "dotnet.native.wasm", - buffer: bins.wasm.buffer + buffer: resources.wasm.content! }], - assembly: bins.assemblies.map(a => ({ + assembly: resources.assemblies.map(a => ({ name: a.name, virtualPath: a.name, - buffer: a.content.buffer.slice(a.content.byteOffset, a.content.byteOffset + a.content.byteLength) + buffer: a.content! })) } }, - create: vi.fn(async () => { - const bootsharp = (await import("../cs/Test/bin/sideload")).default; - const dotnet = (await bootsharp.dotnet.getMain(root)).dotnet; - return await dotnet.withConfig(customs.config!).create(); - }), + resources, + create: vi.fn(async () => await bootsharp.dotnet.withConfig(customs.config!).create()), import: vi.fn(), run: vi.fn(), export: vi.fn() @@ -210,45 +126,17 @@ describe("boot", () => { expect(customs.export).toHaveBeenCalledOnce(); }); it("can boot when program has no exports", async () => { - const { side: { bootsharp }, root } = await setup(); + const { bootsharp, resources } = await setup(); const options: BootOptions = { - create: vi.fn(async () => { - const cfg = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); - const dotnet = (await bootsharp.dotnet.getMain(root)).dotnet; - const runtime = await dotnet.withConfig(cfg).create(); - runtime.getAssemblyExports = () => Promise.resolve({ Bootsharp: { Generated: { Instances: { DisposeExported: () => {} } } } }); + resources, + create: vi.fn(async cfg => { + const runtime = await bootsharp.dotnet.withConfig(cfg).create(); + runtime.getAssemblyExports = () => Promise.resolve({ + Bootsharp: { Generated: { Instances: { DisposeExported: () => {} } } } + }); return runtime; - }), - root + }) }; await bootsharp.boot(options); }); - it("resolves worker module in multithreading mode", async () => { - const { side: { bootsharp }, root } = await setup(); - vi.doMock("../cs/Test/bin/sideload/dotnet.g", () => ({ mt: true })); - const config = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); - expect(config.resources!.jsModuleWorker?.length).toStrictEqual(1); - vi.doUnmock("../cs/Test/bin/sideload/dotnet.g"); - }); -}); - -describe("boot status", () => { - it("is standby by default", async () => { - const { side: { bootsharp } } = await setup(); - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); - }); - it("transitions to booting and then to booted", async () => { - const { side: { bootsharp }, root } = await setup(); - const promise = bootsharp.boot({ root }); - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booting); - await promise; - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); - }); - it("transitions to standby on exit", async () => { - const { side: { bootsharp }, root } = await setup(); - await bootsharp.boot({ root }); - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); - await bootsharp.exit(); - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); - }); }); diff --git a/src/js/test/spec/export.spec.ts b/src/js/test/spec/export.spec.ts index 97178f1e..1a00b346 100644 --- a/src/js/test/spec/export.spec.ts +++ b/src/js/test/spec/export.spec.ts @@ -1,16 +1,14 @@ import { describe, it, expect } from "vitest"; -import { embedded, getDeclarations } from "../cs"; +import { bootsharp, getDeclarations } from "../cs"; describe("export", () => { it("exports bootsharp api", () => { - expect(embedded.boot).toBeTypeOf("function"); - expect(embedded.exit).toBeTypeOf("function"); - expect(embedded.resources).toBeTypeOf("object"); + expect(bootsharp.boot).toBeTypeOf("function"); + expect(bootsharp.exit).toBeTypeOf("function"); + expect(bootsharp.resources).toBeTypeOf("object"); }); - it("exports dotnet modules", () => { - expect(embedded.dotnet.getMain).toBeTypeOf("function"); - expect(embedded.dotnet.getNative).toBeTypeOf("function"); - expect(embedded.dotnet.getRuntime).toBeTypeOf("function"); + it("exports dotnet host builder", () => { + expect(bootsharp.dotnet.withConfig).toBeTypeOf("function"); }); it("exports documentation declarations", () => { expect(getDeclarations()).toContain(`Sample class documentation.`); diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 7a55aece..23ef8276 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -1,5 +1,5 @@ import { describe, it, beforeAll, expect, vi } from "vitest"; -import { Event, Test, bootSideload } from "../cs"; +import { Event, Test, bootRuntime } from "../cs"; const TrackType = Test.Library.TrackType; @@ -30,7 +30,7 @@ describe("while bootsharp is not booted", () => { }); describe("while bootsharp is booted", () => { - beforeAll(bootSideload); + beforeAll(bootRuntime); it("JS functions are unassigned by default", () => { expect(Test.Platform.throwJS).toBeUndefined(); expect(Test.Static.importedFunction).toBeUndefined(); @@ -219,6 +219,9 @@ describe("while bootsharp is booted", () => { it("can catch dotnet exceptions", () => { expect(() => Test.Platform.throwCS("bar")).throw("bar"); }); + it("can catch dotnet exceptions from async methods", async () => { + await expect(Test.Platform.throwCSAsync("baz")).rejects.toThrow("baz"); + }); it("maps enums by both indexes and strings", () => { expect(Test.Static.Enum[1]).toStrictEqual("One"); expect(Test.Static.Enum[2]).toStrictEqual("Two"); diff --git a/src/js/test/spec/platform.spec.ts b/src/js/test/spec/platform.spec.ts index ff47a446..70194df4 100644 --- a/src/js/test/spec/platform.spec.ts +++ b/src/js/test/spec/platform.spec.ts @@ -1,9 +1,9 @@ import { describe, it, beforeAll, expect } from "vitest"; import { WebSocket, WebSocketServer } from "ws"; -import { Test, bootSideload, any } from "../cs"; +import { Test, bootRuntime } from "../cs"; describe("platform", () => { - beforeAll(bootSideload); + beforeAll(bootRuntime); it("can provide unique guid", () => { const guid1 = Test.Platform.getGuid(); const guid2 = Test.Platform.getGuid(); @@ -19,7 +19,7 @@ describe("platform", () => { it("can communicate via websocket", async () => { // .NET requires ws package when running on node: // https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md#websocket - any(global).WebSocket = WebSocket; + (>global).WebSocket = WebSocket; const wss = new WebSocketServer({ port: 0 }); wss.on("connection", socket => socket.on("message", socket.send)); await new Promise(resolve => wss.once("listening", resolve)); diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 13736b37..fb57b9a0 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, it } from "vitest"; -import { Test, bootSideload } from "../cs"; +import { Test, bootRuntime } from "../cs"; describe("serialization", () => { - beforeAll(bootSideload); + beforeAll(bootRuntime); it("can echo primitives", () => { const input: Test.Primitives = { boolean: true, @@ -38,12 +38,28 @@ describe("serialization", () => { expect(Test.Serialization.echoPrimitives([input, null])).toStrictEqual([expected, null]); expect(Test.Serialization.echoPrimitives(undefined)).toBeNull(); }); + it("can echo primitives with all nullable fields omitted", () => { + const input: Test.Primitives = { + boolean: false, byte: 0, sByte: 0, positiveSByte: 0, + int16: 0, uInt16: 0, int32: 0, uInt32: 0, + int64: 0n, uInt64: 0, intPtr: 0, + single: 0, double: 0, decimal: 0, + char: "\0", emptyChar: "\0", missingChar: "\0", + dateTime: new Date(0), dateTimeOffset: new Date(0) + }; + expect(Test.Serialization.echoPrimitives([input])).toStrictEqual([input]); + }); it("can echo unions", () => { const a: Test.Union = { shared: "A", a: { string: "*", map: new Map([["a", null], ["b", 7]]) } }; const b: Test.Union = { shared: "B", b: { ints: [], strings: ["foo", "bar"], times: [new Date()] } }; expect(Test.Serialization.echoUnions([a, b, null])).toStrictEqual([a, b, null]); expect(Test.Serialization.echoUnions(undefined)).toBeNull(); }); + it("can echo unions with all nullable fields omitted", () => { + const a: Test.Union = { shared: "A", a: {} }; + const b: Test.Union = { shared: "B", b: { strings: ["x"], times: [new Date()] } }; + expect(Test.Serialization.echoUnions([a, b])).toStrictEqual([a, b]); + }); it("computes expression properties on the C# side", () => { expect(Test.Serialization.echoComputed({ id: "foo", count: 7, summary: "ignored" })) .toStrictEqual({ id: "foo", count: 7, summary: "foo:7" }); diff --git a/src/js/test/tsconfig.json b/src/js/test/tsconfig.json new file mode 100644 index 00000000..9435f4d7 --- /dev/null +++ b/src/js/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "types": ["node", "vitest"] + }, + "include": ["cs.ts", "spec", "types"] +} diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index f8019424..077210f5 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,9 +1,8 @@ { "compilerOptions": { + "rootDir": "src", "target": "esnext", "module": "esnext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, "strict": true }, "include": ["src"] From 8ea50110d241c0f6edda90d11783c5e9cf6de218 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 9 May 2026 23:51:39 +0300 Subject: [PATCH 02/19] remove unused --- src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 7c46818d..9555ce80 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -82,7 +82,6 @@ public static NullabilityInfo GetNullity (EventInfo evt, ParameterInfo param) return GetNullity(param); } - public static bool IsNullable (Type type) => IsNullable(type, out _); public static bool IsNullable (Type type, NullabilityInfo? info) => IsNullable(type, info, out _); public static bool IsNullable (Type type, [NotNullWhen(true)] out Type? value) => IsNullable(type, null, out value); public static bool IsNullable (Type type, NullabilityInfo? info, [NotNullWhen(true)] out Type? value) From 0a420a010152c741dace218212cde46fb6c63ae8 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 15:00:59 +0300 Subject: [PATCH 03/19] restore monkey patching --- .../Bootsharp.Publish/Pack/ModulePatcher.cs | 25 +++++++++++++++++++ src/cs/Directory.Build.props | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs index b870b9ee..13c4a897 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs @@ -13,6 +13,7 @@ public void Patch () { RemoveMaps(); RemoveWasmNag(); + PacifyBundlers(); } private void RemoveMaps () @@ -33,4 +34,28 @@ private void RemoveWasmNag () RegexOptions.Compiled | RegexOptions.CultureInvariant) .Replace(File.ReadAllText(dotnet, Encoding.UTF8), "true"), Encoding.UTF8); } + + private void PacifyBundlers () + { + // Neutralizes the bundler-offending code in the .NET and Emscripten's generated ES modules. + // Rel: https://github.com/elringus/bootsharp/issues/139 + const string ignore = "/*@vite-ignore*//*webpackIgnore:true*/"; + const string url = + """ + ((typeof window === "object" && "Deno" in window && Deno.build.os === "windows") || (typeof process === "object" && process.platform === "win32")) ? "file://dotnet.native.wasm" : "file:///dotnet.native.wasm" + """; + File.WriteAllText(dotnet, File.ReadAllText(dotnet, Encoding.UTF8) + .Replace("import.meta.url", url) + .Replace("import(", $"import({ignore}"), Encoding.UTF8); + File.WriteAllText(runtime, File.ReadAllText(runtime, Encoding.UTF8) + .Replace("import(", $"import({ignore}"), Encoding.UTF8); + File.WriteAllText(native, File.ReadAllText(native, Encoding.UTF8) + .Replace("var _scriptDir = import.meta.url", "var _scriptDir = \"file:/\"") + .Replace("require('url').fileURLToPath(new URL('./', import.meta.url))", "\"./\"") + .Replace("require(\"url\").fileURLToPath(new URL(\"./\",import.meta.url))", "\"./\"") + .Replace("new URL('dotnet.native.wasm', import.meta.url).href", "\"file:/\"") + .Replace("new URL(\"dotnet.native.wasm\",import.meta.url).href", "\"file:/\"") + .Replace("import.meta.url", url) + .Replace("import(", $"import({ignore}"), Encoding.UTF8); + } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index d4354a7e..76cb9e30 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.198 + 0.8.0-alpha.200 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 624b6e3ca8bfa027e04112cc16e9a014e8854893 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 15:39:57 +0300 Subject: [PATCH 04/19] fix patcher --- .../Bootsharp.Publish/Pack/ModulePatcher.cs | 37 +++++++------------ src/cs/Directory.Build.props | 2 +- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs index 13c4a897..04a3aabe 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs @@ -1,5 +1,6 @@ using System.Text; using System.Text.RegularExpressions; +using static System.Text.RegularExpressions.RegexOptions; namespace Bootsharp.Publish; @@ -20,8 +21,7 @@ private void RemoveMaps () { // Microsoft bundles .NET JavaScript sources pre-minified/uglified with source maps // referencing upstream sources we don't publish with the package. - var regex = new Regex(@"^\s*//# sourceMappingURL=.*?\.map\s*$\r?\n?", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline); + var regex = new Regex(@"^\s*//# sourceMappingURL=.*?\.map\s*$\r?\n?", Compiled | CultureInvariant | Multiline); File.WriteAllText(dotnet, regex.Replace(File.ReadAllText(dotnet, Encoding.UTF8), ""), Encoding.UTF8); File.WriteAllText(runtime, regex.Replace(File.ReadAllText(runtime, Encoding.UTF8), ""), Encoding.UTF8); File.WriteAllText(native, regex.Replace(File.ReadAllText(native, Encoding.UTF8), ""), Encoding.UTF8); @@ -30,32 +30,21 @@ private void RemoveMaps () private void RemoveWasmNag () { // Removes "WebAssembly resource does not have the expected content type..." warning. - File.WriteAllText(dotnet, new Regex("""(?:[$\w]+\.)*[$\w]+\(\s*(['"])WebAssembly resource does not have the expected content type \\?"application/wasm\\?", so falling back to slower ArrayBuffer instantiation\.\1\s*\)""", - RegexOptions.Compiled | RegexOptions.CultureInvariant) - .Replace(File.ReadAllText(dotnet, Encoding.UTF8), "true"), Encoding.UTF8); + var regex = new Regex(@"\w+\(['""]WebAssembly resource does not have[^)]+\)", Compiled | CultureInvariant); + File.WriteAllText(dotnet, regex.Replace(File.ReadAllText(dotnet, Encoding.UTF8), "true"), Encoding.UTF8); } private void PacifyBundlers () { // Neutralizes the bundler-offending code in the .NET and Emscripten's generated ES modules. - // Rel: https://github.com/elringus/bootsharp/issues/139 - const string ignore = "/*@vite-ignore*//*webpackIgnore:true*/"; - const string url = - """ - ((typeof window === "object" && "Deno" in window && Deno.build.os === "windows") || (typeof process === "object" && process.platform === "win32")) ? "file://dotnet.native.wasm" : "file:///dotnet.native.wasm" - """; - File.WriteAllText(dotnet, File.ReadAllText(dotnet, Encoding.UTF8) - .Replace("import.meta.url", url) - .Replace("import(", $"import({ignore}"), Encoding.UTF8); - File.WriteAllText(runtime, File.ReadAllText(runtime, Encoding.UTF8) - .Replace("import(", $"import({ignore}"), Encoding.UTF8); - File.WriteAllText(native, File.ReadAllText(native, Encoding.UTF8) - .Replace("var _scriptDir = import.meta.url", "var _scriptDir = \"file:/\"") - .Replace("require('url').fileURLToPath(new URL('./', import.meta.url))", "\"./\"") - .Replace("require(\"url\").fileURLToPath(new URL(\"./\",import.meta.url))", "\"./\"") - .Replace("new URL('dotnet.native.wasm', import.meta.url).href", "\"file:/\"") - .Replace("new URL(\"dotnet.native.wasm\",import.meta.url).href", "\"file:/\"") - .Replace("import.meta.url", url) - .Replace("import(", $"import({ignore}"), Encoding.UTF8); + // We handle the imports and WASM loading ourselves, so their hacks are a dead code. + var imp = new Regex(@"(? - 0.8.0-alpha.200 + 0.8.0-alpha.202 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From a439b926a4689f207f96d7cef425444b852a47bd Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 16:21:41 +0300 Subject: [PATCH 05/19] publish package to projdir --- src/cs/Bootsharp/Build/Bootsharp.props | 2 +- src/cs/Bootsharp/Build/Bootsharp.targets | 5 ++--- src/cs/Directory.Build.props | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cs/Bootsharp/Build/Bootsharp.props b/src/cs/Bootsharp/Build/Bootsharp.props index 65ba4a3d..f6b98a7c 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.props +++ b/src/cs/Bootsharp/Build/Bootsharp.props @@ -23,7 +23,7 @@ - + diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 5cdd7bc0..59f213cc 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -92,7 +92,7 @@ $(BsBaseOutputPath)$(BootsharpName) $(BootsharpPublishDirectory)/types $(BootsharpPublishDirectory)/bin - $(BootsharpPublishDirectory) + $(MSBuildProjectDirectory) $([System.String]::Equals('$(InvariantGlobalization)', 'false')) @@ -156,8 +156,7 @@ - paths) + { + foreach (var path in paths) + File.WriteAllText(path, pattern.Replace(File.ReadAllText(path, Encoding.UTF8), with), Encoding.UTF8); } } diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 59f213cc..85e7a710 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -156,7 +156,7 @@ - - 0.8.0-alpha.204 + 0.8.0-alpha.211 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 400011233dd75cc7c61926a5a8b20398c2c54ee2 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 17:33:10 +0300 Subject: [PATCH 07/19] fix e2e --- src/js/test/cs.ts | 6 +++--- src/js/test/cs/Test/package.json | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/js/test/cs/Test/package.json diff --git a/src/js/test/cs.ts b/src/js/test/cs.ts index 1c7b7377..089c6c8d 100644 --- a/src/js/test/cs.ts +++ b/src/js/test/cs.ts @@ -1,11 +1,11 @@ import assert from "node:assert"; import { resolve } from "node:path"; import { readFileSync, existsSync } from "node:fs"; -import type { BootResources } from "./cs/Test/bin/bootsharp"; -import bootsharp, { Test } from "./cs/Test/bin/bootsharp"; +import type { BootResources } from "./cs/Test"; +import bootsharp, { Test } from "./cs/Test"; export { bootsharp, Test }; -export * from "./cs/Test/bin/bootsharp"; +export * from "./cs/Test"; export const resources: BootResources = loadResources(); diff --git a/src/js/test/cs/Test/package.json b/src/js/test/cs/Test/package.json new file mode 100644 index 00000000..1753e5b2 --- /dev/null +++ b/src/js/test/cs/Test/package.json @@ -0,0 +1,10 @@ +{ + "name": "bootsharp", + "type": "module", + "main": "bin/bootsharp/index.mjs", + "types": "bin/bootsharp/types/index.d.mts", + "browser": { + "url": false, "fs": false, "process": false, + "module": false, "path": false, "crypto": false + } +} From 0c4a04666c38fbb133b39933bb408a1a8bfc7491 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 18:33:44 +0300 Subject: [PATCH 08/19] simplify boot sig --- .../Pack/ResourceTest.cs | 12 ++-- .../Pack/ResourceGenerator.cs | 15 ++-- src/cs/Directory.Build.props | 2 +- src/js/src/boot.mts | 16 ++--- src/js/src/config.mts | 20 +++--- src/js/src/generated/resources.g.mts | 4 +- src/js/src/index.mts | 6 +- src/js/src/resources.mts | 70 +++++++++++-------- src/js/test/cs.ts | 22 +++--- src/js/test/spec/boot.spec.ts | 42 ++++++----- src/js/test/spec/export.spec.ts | 2 +- 11 files changed, 110 insertions(+), 101 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs index 46b2dc53..5832023c 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs @@ -20,8 +20,8 @@ public void WhenDebugEnabledDebugArtifactsIncluded () Project.WriteFile("Foo.pdb", "MockPdbContent"); Project.WriteFile("dotnet.native.js.symbols", "MockSymbolsContent"); Execute(); - Contains("""{ name: "Foo.pdb" }"""); - Contains("""{ name: "dotnet.native.js.symbols" }"""); + Contains("Foo.pdb"); + Contains("dotnet.native.js.symbols"); } [Fact] @@ -32,8 +32,8 @@ public void WhenDebugDisabledDebugArtifactsNotIncluded () Project.WriteFile("Foo.pdb", "MockPdbContent"); Project.WriteFile("dotnet.native.js.symbols", "MockSymbolsContent"); Execute(); - DoesNotContain("""{ name: "Foo.pdb" }"""); - DoesNotContain("""{ name: "dotnet.native.js.symbols" }"""); + DoesNotContain("Foo.pdb"); + DoesNotContain("dotnet.native.js.symbols"); } [Fact] @@ -43,7 +43,7 @@ public void WhenGlobalizationEnabledIcuIncluded () AddAssembly("Foo.dll"); Project.WriteFile("icudt.dat", "MockIcuContent"); Execute(); - Contains("""{ name: "icudt.dat" }"""); + Contains("icudt.dat"); } [Fact] @@ -53,6 +53,6 @@ public void WhenGlobalizationDisabledIcuNotIncluded () AddAssembly("Foo.dll"); Project.WriteFile("icudt.dat", "MockIcuContent"); Execute(); - DoesNotContain("""{ name: "icudt.dat" }"""); + DoesNotContain("icudt.dat"); } } diff --git a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs index 7f9cadbe..e698f457 100644 --- a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs @@ -11,19 +11,19 @@ internal sealed class ResourceGenerator (string entryAssemblyName, bool debug, b public string Generate (string buildDir, string debugDir) { foreach (var path in Directory.GetFiles(buildDir, "*.wasm").Order()) - if (path.EndsWith("dotnet.native.wasm")) wasm = BuildResource(path); - else assemblies.Add(BuildResource(path)); + if (path.EndsWith("dotnet.native.wasm")) wasm = BuildResourceName(path); + else assemblies.Add(BuildResourceName(path)); if (g11n) { foreach (var path in Directory.GetFiles(buildDir, "*.dat").Order()) - icu.Add(BuildResource(path)); + icu.Add(BuildResourceName(path)); } if (debug) { foreach (var path in Directory.GetFiles(debugDir, "*.symbols").Order()) - symbols.Add(BuildResource(path)); + symbols.Add(BuildResourceName(path)); foreach (var path in Directory.GetFiles(debugDir, "*.pdb").Order()) - pdb.Add(BuildResource(path)); + pdb.Add(BuildResourceName(path)); } return $$""" @@ -46,9 +46,8 @@ public string Generate (string buildDir, string debugDir) """; } - private string BuildResource (string path) + private string BuildResourceName (string path) { - var name = Path.GetFileName(path); - return $$"""{ name: "{{name}}" }"""; + return $"\"{Path.GetFileName(path)}\""; } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 316c9d93..a5694eda 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.211 + 0.8.0-alpha.214 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/src/boot.mts b/src/js/src/boot.mts index 8c5bc6b4..55ca497d 100644 --- a/src/js/src/boot.mts +++ b/src/js/src/boot.mts @@ -15,10 +15,8 @@ export enum BootStatus { Booted } -/** Configuration of the runtime boot process. */ +/** Custom configuration of the runtime boot process. */ export type BootOptions = { - /** Resources required to boot the runtime. */ - readonly resources: BootResources; /** Custom runtime configuration. */ readonly config?: RuntimeConfig; /** Customization hook for creating the runtime instance. */ @@ -39,14 +37,14 @@ export function getStatus(): BootStatus { } /** Initializes the runtime and binds C# APIs. - * @param opt Either URL to the boot resources root (eg, /bin) or a full configuration. + * @param resources Either URL to the boot resources root (eg, /bin) or the preloaded content. + * @param options Allows customizing the boot process and the runtime behaviour. * @return Promise that resolves into the runtime instance when the initialization is finished. */ -export async function boot(opt: string | BootOptions): Promise { +export async function boot(resources: string | BootResources, options?: BootOptions): Promise { if (status === BootStatus.Booted) throw Error("Failed to boot the C# runtime: already booted."); if (status === BootStatus.Booting) throw Error("Failed to boot the C# runtime: already booting."); status = BootStatus.Booting; - const options = typeof opt === "string" ? { resources: await fetchResources(opt) } : opt; - const runtime = await createRuntime(options); + const runtime = await createRuntime(resources, options ?? {}); status = BootStatus.Booted; return runtime; } @@ -63,8 +61,8 @@ export async function exit(code?: number, reason?: string): Promise { /* v8 ignore stop -- @preserve */ } -async function createRuntime(opt: BootOptions) { - const cfg = opt.config ?? buildConfig(opt.resources); +async function createRuntime(res: string | BootResources, opt: BootOptions) { + const cfg = opt.config ?? buildConfig(typeof res === "string" ? await fetchResources(res) : res); const runtime = await opt.create?.(cfg) || await app.dotnet.withConfig(cfg).create(); setRuntime(runtime); if (opt.import) await opt.import(runtime); else bindImports(runtime); diff --git a/src/js/src/config.mts b/src/js/src/config.mts index 4a23bbe7..a3ac1f77 100644 --- a/src/js/src/config.mts +++ b/src/js/src/config.mts @@ -1,5 +1,5 @@ -import type { RuntimeConfig, Asset, WasmAsset, AssemblyAsset, IcuAsset, PdbAsset, SymbolsAsset, ModuleAsset } from "./dotnet/index.mjs"; -import type { BinaryResource, BootResources } from "./resources.mjs"; +import { RuntimeConfig, Asset, WasmAsset, AssemblyAsset, IcuAsset, PdbAsset, SymbolsAsset, ModuleAsset } from "./dotnet/index.mjs"; +import { BinaryResource, BootResources, manifest } from "./resources.mjs"; import * as nativeModule from "./dotnet/dotnet.native.js"; import * as runtimeModule from "./dotnet/dotnet.runtime.js"; @@ -7,17 +7,17 @@ import * as runtimeModule from "./dotnet/dotnet.runtime.js"; export function buildConfig(resources: BootResources): RuntimeConfig { return { resources: { - wasmNative: [resolveAsset(resources.wasm)], + wasmNative: [resolveAsset({ name: manifest.wasm, content: resources.wasm })], jsModuleNative: [resolveModule("dotnet.native.js", nativeModule)], jsModuleRuntime: [resolveModule("dotnet.runtime.js", runtimeModule)], - assembly: resources.assemblies.map(resolveAsset), - icu: resources.icu.map(resolveAsset), - wasmSymbols: resources.symbols.map(resolveSymbols), - pdb: resources.pdb.map(resolveAsset) + assembly: resources.assemblies?.map(resolveAsset), + icu: resources.icu?.map(resolveAsset), + wasmSymbols: resources.symbols?.map(resolveSymbols), + pdb: resources.pdb?.map(resolveAsset) }, - mainAssemblyName: resources.entryAssemblyName, + mainAssemblyName: manifest.entryAssemblyName, globalizationMode: resolveGlobalizationMode(), - debugLevel: resources.symbols.length > 0 ? -1 : undefined + debugLevel: resources.symbols ? -1 : undefined }; function resolveModule(name: string, exports: unknown): ModuleAsset { @@ -42,7 +42,7 @@ export function buildConfig(resources: BootResources): RuntimeConfig { } function resolveGlobalizationMode(): RuntimeConfig["globalizationMode"] { - if (resources.icu.length === 0) return "invariant" as never; + if (!resources.icu) return "invariant" as never; if (resources.icu.some(res => res.name === "icudt.dat")) return "all" as never; return "sharded" as never; } diff --git a/src/js/src/generated/resources.g.mts b/src/js/src/generated/resources.g.mts index 82fc6873..e03f5467 100644 --- a/src/js/src/generated/resources.g.mts +++ b/src/js/src/generated/resources.g.mts @@ -1,3 +1,3 @@ -import { BootResources } from "../resources.mjs"; +import { BootManifest } from "../resources.mjs"; -export default {} as BootResources; +export default {} as BootManifest; diff --git a/src/js/src/index.mts b/src/js/src/index.mts index aa563bd4..de8c246b 100644 --- a/src/js/src/index.mts +++ b/src/js/src/index.mts @@ -1,5 +1,5 @@ import { boot, exit, getStatus, BootStatus } from "./boot.mjs"; -import { resources } from "./resources.mjs"; +import { manifest } from "./resources.mjs"; import { app } from "./dotnet/index.mjs"; export default { @@ -7,11 +7,11 @@ export default { exit, getStatus, BootStatus, - resources, + manifest, dotnet: app.dotnet }; export * from "./event.mjs"; export * from "./generated/bindings.g.mjs"; export type { BootOptions } from "./boot.mjs"; -export type { BootResources, BinaryResource } from "./resources.mjs"; +export type { BootManifest, BootResources, BinaryResource } from "./resources.mjs"; diff --git a/src/js/src/resources.mts b/src/js/src/resources.mts index 6cc1ccfb..e01277e1 100644 --- a/src/js/src/resources.mts +++ b/src/js/src/resources.mts @@ -1,45 +1,59 @@ import generated from "./generated/resources.g.mjs"; -/** Resources required to boot .NET runtime. */ -export type BootResources = { - /** Compiled .NET WASM runtime module. */ - readonly wasm: BinaryResource; - /** Compiled .NET assemblies. */ - readonly assemblies: BinaryResource[]; +/** Lists resource file names (including extension) required to boot the runtime. */ +export type BootManifest = Readonly<{ + /** Compiled WASM runtime module. */ + wasm: string; + /** Compiled runtime assemblies. */ + assemblies: string[]; /** Globalization data. */ - readonly icu: BinaryResource[]; + icu: string[]; /** WASM debug symbols. */ - readonly symbols: BinaryResource[]; + symbols: string[]; /** PDB debug artifacts. */ - readonly pdb: BinaryResource[]; - /** Name of the entry (main) assembly, with .dll extension. */ - readonly entryAssemblyName: string; -} + pdb: string[]; + /** Name of the entry (main) assembly. */ + entryAssemblyName: string; +}>; + +/** Resources required to boot the runtime. */ +export type BootResources = Readonly<{ + /** Binary content of the compiled WASM runtime module. */ + wasm: ArrayBuffer; + /** Compiled runtime assemblies. */ + assemblies?: BinaryResource[]; + /** Globalization data. */ + icu?: BinaryResource[]; + /** WASM debug symbols. */ + symbols?: BinaryResource[]; + /** PDB debug artifacts. */ + pdb?: BinaryResource[]; +}>; /** Boot resource with binary content. */ -export type BinaryResource = { - /** Name of the binary file, including extension. */ - readonly name: string; +export type BinaryResource = Readonly<{ + /** Name of the file, including extension. */ + name: string; /** Binary content of the file. */ - readonly content: ArrayBuffer; -} + content: ArrayBuffer; +}>; -/** Resources required to boot .NET runtime. */ -export const resources: BootResources = generated; +/** Lists resource names required to boot the runtime. */ +export const manifest: BootManifest = generated; /** Fetches required boot resources from the specified root URL. */ export async function fetchResources(root: string): Promise { const [wasm, assemblies, icu, symbols, pdb] = await Promise.all([ - fetchResource(resources.wasm), - Promise.all(resources.assemblies.map(fetchResource)), - Promise.all(resources.icu.map(fetchResource)), - Promise.all(resources.symbols.map(fetchResource)), - Promise.all(resources.pdb.map(fetchResource)) + fetchResource(manifest.wasm), + Promise.all(manifest.assemblies.map(fetchResource)), + Promise.all(manifest.icu.map(fetchResource)), + Promise.all(manifest.symbols.map(fetchResource)), + Promise.all(manifest.pdb.map(fetchResource)) ]); - return { wasm, assemblies, icu, symbols, pdb, entryAssemblyName: resources.entryAssemblyName }; + return { wasm: wasm.content, assemblies, icu, symbols, pdb }; - async function fetchResource(r: BinaryResource): Promise { - const content = await (await fetch(`${root}/${r.name}`)).arrayBuffer(); - return { name: r.name, content }; + async function fetchResource(name: string): Promise { + const content = await (await fetch(`${root}/${name}`)).arrayBuffer(); + return { name, content }; } } diff --git a/src/js/test/cs.ts b/src/js/test/cs.ts index 089c6c8d..864d1ca6 100644 --- a/src/js/test/cs.ts +++ b/src/js/test/cs.ts @@ -1,17 +1,18 @@ import assert from "node:assert"; import { resolve } from "node:path"; import { readFileSync, existsSync } from "node:fs"; -import type { BootResources } from "./cs/Test"; +import type { BootResources, BinaryResource } from "./cs/Test"; import bootsharp, { Test } from "./cs/Test"; export { bootsharp, Test }; export * from "./cs/Test"; -export const resources: BootResources = loadResources(); +export const resources = loadResources(); +export const manifest = bootsharp.manifest; export async function bootRuntime() { Test.Program.onMainInvoked = () => {}; - await bootsharp.boot({ resources }); + await bootsharp.boot(resources); } export function getDeclarations() { @@ -21,23 +22,22 @@ export function getDeclarations() { function loadResources(): BootResources { return { - wasm: load(bootsharp.resources.wasm.name), - assemblies: bootsharp.resources.assemblies.map(r => load(r.name)), - icu: bootsharp.resources.icu.map(r => load(r.name)), - symbols: bootsharp.resources.symbols.map(r => load(r.name)), - pdb: bootsharp.resources.pdb.map(r => load(r.name)), - entryAssemblyName: bootsharp.resources.entryAssemblyName + wasm: load(bootsharp.manifest.wasm).content, + assemblies: bootsharp.manifest.assemblies.map(load), + icu: bootsharp.manifest.icu.map(load), + symbols: bootsharp.manifest.symbols.map(load), + pdb: bootsharp.manifest.pdb.map(load) }; } -function load(name: string) { +function load(name: string): BinaryResource { const path = resolvePath(`test/cs/Test/bin/bootsharp/bin/${name}`); const bytes = readFileSync(path); const content = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; return { name, content }; } -function resolvePath(path: string) { +function resolvePath(path: string): string { const resolved = resolve(path); assert(existsSync(resolved), `Missing test project artifact: '${path}'. Run 'scripts/compile-test.sh'.`); return resolved; diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 80cafabe..20a65478 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -1,7 +1,7 @@ import { resolve } from "node:path"; import { readFileSync } from "node:fs"; import { describe, expect, it, vi } from "vitest"; -import type { BootOptions } from "../cs/Test/bin/bootsharp"; +import type { BootOptions } from "../cs/Test"; async function setup() { vi.resetModules(); @@ -21,40 +21,40 @@ describe("boot", () => { }); it("transitions to booting and then to booted", async () => { const { bootsharp, resources } = await setup(); - const promise = bootsharp.boot({ resources }); + const promise = bootsharp.boot(resources); expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booting); await promise; expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); }); it("throws when boot invoked while booted", async () => { const { bootsharp, resources } = await setup(); - await bootsharp.boot({ resources }); - await expect(bootsharp.boot({ resources })).rejects.toThrow(/already booted/); + await bootsharp.boot(resources); + await expect(bootsharp.boot(resources)).rejects.toThrow(/already booted/); }); it("throws when boot invoked while booting", async () => { const { bootsharp, resources } = await setup(); - const boot = bootsharp.boot({ resources }); - await expect(bootsharp.boot({ resources })).rejects.toThrow(/already booting/); + const boot = bootsharp.boot(resources); + await expect(bootsharp.boot(resources)).rejects.toThrow(/already booting/); await boot; }); it("invokes program main on boot", async () => { const { bootsharp, resources, Test } = await setup(); - await bootsharp.boot({ resources }); + await bootsharp.boot(resources); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); it("enables debugging when debugging resources are present", async () => { const { bootsharp, resources } = await setup(); - const config = (await bootsharp.boot({ resources })).getConfig(); + const config = (await bootsharp.boot(resources)).getConfig(); expect(config.debugLevel).not.toBeUndefined(); }); it("doesn't enable debugging when debug are absent", async () => { const { bootsharp, resources } = await setup(); - const config = (await bootsharp.boot({ resources: { ...resources, symbols: [], pdb: [] } })).getConfig(); + const config = (await bootsharp.boot({ ...resources, symbols: undefined, pdb: undefined })).getConfig(); expect(config.debugLevel).toBeUndefined(); }); it("uses full globalization mode when full ICU resource is present", async () => { const { bootsharp, resources } = await setup(); - const config = (await bootsharp.boot({ resources })).getConfig(); + const config = (await bootsharp.boot(resources)).getConfig(); expect(config.globalizationMode).toStrictEqual("all"); }); it("uses sharded globalization mode when sharded ICU resource is present", async () => { @@ -64,20 +64,20 @@ describe("boot", () => { return { name, content: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }; }; const icu = ["icudt_CJK.dat", "icudt_EFIGS.dat", "icudt_no_CJK.dat"].map(load); - const config = (await bootsharp.boot({ resources: { ...resources, icu } })).getConfig(); + const config = (await bootsharp.boot({ ...resources, icu })).getConfig(); expect(config.globalizationMode).toStrictEqual("sharded"); }); it("disables globalization when ICU resources are absent", async () => { const { bootsharp, resources } = await setup(); - const config = (await bootsharp.boot({ resources: { ...resources, icu: [] } })).getConfig(); + const config = (await bootsharp.boot({ ...resources, icu: undefined })).getConfig(); expect(config.globalizationMode).toStrictEqual("invariant"); }); it("fetches resources when root is specified", async () => { const { bootsharp, resources, Test } = await setup(); - const bin = [resources.wasm, ...resources.assemblies, ...resources.icu, ...resources.symbols, ...resources.pdb]; + const bin = [...resources.assemblies!, ...resources.icu!, ...resources.symbols!, ...resources.pdb!]; const fetchSpy = vi.fn(url => { const name = url.substring(url.lastIndexOf("/") + 1); - const content = bin.find(r => r.name === name)!.content; + const content = bin.find(r => r.name === name)?.content ?? resources.wasm; return Promise.resolve({ arrayBuffer: () => Promise.resolve(content) }); }); const fetch = global.fetch; @@ -89,10 +89,10 @@ describe("boot", () => { expect(fetchSpy).toHaveBeenCalledWith("/bin/Bootsharp.Common.wasm"); }); it("respects boot customs", async () => { - const { bootsharp, resources } = await setup(); + const { bootsharp, resources, manifest } = await setup(); const customs: BootOptions = { config: { - mainAssemblyName: resources.entryAssemblyName, + mainAssemblyName: manifest.entryAssemblyName, resources: { jsModuleRuntime: [{ name: "dotnet.runtime.js", @@ -104,22 +104,21 @@ describe("boot", () => { }], wasmNative: [{ name: "dotnet.native.wasm", - buffer: resources.wasm.content! + buffer: resources.wasm }], - assembly: resources.assemblies.map(a => ({ + assembly: resources.assemblies?.map(a => ({ name: a.name, virtualPath: a.name, buffer: a.content! })) } }, - resources, create: vi.fn(async () => await bootsharp.dotnet.withConfig(customs.config!).create()), import: vi.fn(), run: vi.fn(), export: vi.fn() }; - await bootsharp.boot(customs); + await bootsharp.boot("does not matter with the custom create hook", customs); expect(customs.create).toHaveBeenCalledWith(customs.config); expect(customs.import).toHaveBeenCalledOnce(); expect(customs.run).toHaveBeenCalledOnce(); @@ -128,7 +127,6 @@ describe("boot", () => { it("can boot when program has no exports", async () => { const { bootsharp, resources } = await setup(); const options: BootOptions = { - resources, create: vi.fn(async cfg => { const runtime = await bootsharp.dotnet.withConfig(cfg).create(); runtime.getAssemblyExports = () => Promise.resolve({ @@ -137,6 +135,6 @@ describe("boot", () => { return runtime; }) }; - await bootsharp.boot(options); + await bootsharp.boot(resources, options); }); }); diff --git a/src/js/test/spec/export.spec.ts b/src/js/test/spec/export.spec.ts index 1a00b346..fe4abcdc 100644 --- a/src/js/test/spec/export.spec.ts +++ b/src/js/test/spec/export.spec.ts @@ -5,7 +5,7 @@ describe("export", () => { it("exports bootsharp api", () => { expect(bootsharp.boot).toBeTypeOf("function"); expect(bootsharp.exit).toBeTypeOf("function"); - expect(bootsharp.resources).toBeTypeOf("object"); + expect(bootsharp.manifest).toBeTypeOf("object"); }); it("exports dotnet host builder", () => { expect(bootsharp.dotnet.withConfig).toBeTypeOf("function"); From 2b4195a37fd2af55145fe0be53f3b2d9f658488c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 19:20:43 +0300 Subject: [PATCH 09/19] fix imports --- samples/minimal/README.md | 2 +- samples/minimal/cs/Program.cs | 2 +- samples/minimal/cs/package.json | 14 ++++++++++++++ samples/minimal/main.mjs | 2 +- src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs | 8 ++++++++ src/cs/Bootsharp/Build/PackageTemplate.json | 12 ++++++++---- src/cs/Directory.Build.props | 2 +- 7 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 samples/minimal/cs/package.json diff --git a/samples/minimal/README.md b/samples/minimal/README.md index e4ca0f95..ccadab74 100644 --- a/samples/minimal/README.md +++ b/samples/minimal/README.md @@ -1,7 +1,7 @@ Minimal example on using Bootsharp in web browsers and popular JavaScript runtimes. - Run `dotnet publish cs`; -- Run `node main.mjs` to test in [Node](https://nodejs.org); +- Run `node main.mjs` to test in [Node](https://nodejs.org) (blocker: https://github.com/elringus/bootsharp/pull/203); - Run `deno run main.mjs` to test in [Deno](https://deno.com); - Run `bun main.mjs` to test in [Bun](https://bun.sh); - Run an HTML server (eg, `npx serve`) to test in browser. diff --git a/samples/minimal/cs/Program.cs b/samples/minimal/cs/Program.cs index 9ff26511..bb600f72 100644 --- a/samples/minimal/cs/Program.cs +++ b/samples/minimal/cs/Program.cs @@ -4,7 +4,7 @@ public static partial class Program { [Export] // Used in JS as Program.onMainInvoked.subscribe(...) - public static event Action? OnMainInvoked; + public static event Action OnMainInvoked; public static void Main () { diff --git a/samples/minimal/cs/package.json b/samples/minimal/cs/package.json new file mode 100644 index 00000000..42e6b326 --- /dev/null +++ b/samples/minimal/cs/package.json @@ -0,0 +1,14 @@ +{ + "name": "bootsharp", + "type": "module", + "exports": "./bin/bootsharp/index.mjs", + "types": "./bin/bootsharp/types/index.d.mts", + "browser": { + "fs": false, + "url": false, + "path": false, + "module": false, + "crypto": false, + "process": false + } +} diff --git a/samples/minimal/main.mjs b/samples/minimal/main.mjs index 9d597e41..5c56a5b8 100644 --- a/samples/minimal/main.mjs +++ b/samples/minimal/main.mjs @@ -12,7 +12,7 @@ Program.getFrontendName = () => Program.onMainInvoked.subscribe(console.log); // Initializing dotnet runtime and invoking entry point. -await bootsharp.boot(); +await bootsharp.boot(import.meta.resolve("./cs/bin/bootsharp/bin")); // Invoking 'Program.GetBackendName' C# method. console.log(`Hello ${Program.getBackendName()}!`); diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs index 10f7b70a..b3c5a05d 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs @@ -14,6 +14,7 @@ public void Patch () { RemoveMaps(); RemoveWasmNag(); + QualifyImports(); PacifyBundlers(); } @@ -32,6 +33,13 @@ private void RemoveWasmNag () "true", dotnet); } + private void QualifyImports () + { + // Deno requires 'node:' prefix on Node built-ins. + Rewrite(new(@"import\(([^""')]*)['""](?!node:)(url|fs|process|module|path|crypto)['""]\)", CultureInvariant), + "import($1\"node:$2\")", dotnet, runtime, native); + } + private void PacifyBundlers () { // Neutralizes the bundler-offending code in the .NET and Emscripten's generated ES modules. diff --git a/src/cs/Bootsharp/Build/PackageTemplate.json b/src/cs/Bootsharp/Build/PackageTemplate.json index 44de6723..e6f52c25 100644 --- a/src/cs/Bootsharp/Build/PackageTemplate.json +++ b/src/cs/Bootsharp/Build/PackageTemplate.json @@ -1,10 +1,14 @@ { "name": "%MODULE_NAME%", "type": "module", - "main": "%MODULE_DIR%/index.mjs", - "types": "%TYPES_DIR%/index.d.mts", + "exports": "./%MODULE_DIR%/index.mjs", + "types": "./%TYPES_DIR%/index.d.mts", "browser": { - "url": false, "fs": false, "process": false, - "module": false, "path": false, "crypto": false + "fs": false, + "url": false, + "path": false, + "module": false, + "crypto": false, + "process": false } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index a5694eda..1561205f 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.214 + 0.8.0-alpha.217 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From da4e9b493f4f34562e21b2cdd5f23536a0b0425a Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 19:29:07 +0300 Subject: [PATCH 10/19] update samples --- samples/bench/bootsharp/init.mjs | 2 +- samples/bench/bootsharp/package.json | 14 ++++++++++++++ samples/trimming/cs/package.json | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 samples/bench/bootsharp/package.json create mode 100644 samples/trimming/cs/package.json diff --git a/samples/bench/bootsharp/init.mjs b/samples/bench/bootsharp/init.mjs index bbc4c461..73c62ef8 100644 --- a/samples/bench/bootsharp/init.mjs +++ b/samples/bench/bootsharp/init.mjs @@ -6,7 +6,7 @@ export async function init() { Imported.getNumber = getNumber; Imported.getStruct = getStruct; - await bootsharp.boot(); + await bootsharp.boot(import.meta.resolve("./bin/bootsharp/bin")); return { ...Exported }; } diff --git a/samples/bench/bootsharp/package.json b/samples/bench/bootsharp/package.json new file mode 100644 index 00000000..42e6b326 --- /dev/null +++ b/samples/bench/bootsharp/package.json @@ -0,0 +1,14 @@ +{ + "name": "bootsharp", + "type": "module", + "exports": "./bin/bootsharp/index.mjs", + "types": "./bin/bootsharp/types/index.d.mts", + "browser": { + "fs": false, + "url": false, + "path": false, + "module": false, + "crypto": false, + "process": false + } +} diff --git a/samples/trimming/cs/package.json b/samples/trimming/cs/package.json new file mode 100644 index 00000000..42e6b326 --- /dev/null +++ b/samples/trimming/cs/package.json @@ -0,0 +1,14 @@ +{ + "name": "bootsharp", + "type": "module", + "exports": "./bin/bootsharp/index.mjs", + "types": "./bin/bootsharp/types/index.d.mts", + "browser": { + "fs": false, + "url": false, + "path": false, + "module": false, + "crypto": false, + "process": false + } +} From 91c8f2686ca0577f8cf68984d541ba8dd5959011 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 19:31:59 +0300 Subject: [PATCH 11/19] update trimming sample --- samples/trimming/main.mjs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/samples/trimming/main.mjs b/samples/trimming/main.mjs index 6780a363..aed5aee7 100644 --- a/samples/trimming/main.mjs +++ b/samples/trimming/main.mjs @@ -1,5 +1,4 @@ import bootsharp, { Program } from "./cs/bin/bootsharp/index.mjs"; -import { pathToFileURL } from "node:url"; import fs from "node:fs/promises"; import zlib from "node:zlib"; import util from "node:util"; @@ -8,15 +7,8 @@ import path from "node:path"; console.log(`Binary size: ${await measure("./cs/bin/bootsharp/bin")}KB`); console.log(`Brotli size: ${await measure("./cs/bin/bootsharp/bro")}KB`); -const resources = { ...bootsharp.resources }; -await Promise.all([ - fetchBro(resources.wasm), - ...resources.assemblies.map(fetchBro) -]); - Program.log = console.log; -const root = pathToFileURL(path.resolve("./cs/bin/bootsharp/bin")); -await bootsharp.boot({ root, resources }); +await bootsharp.boot({ wasm: await loadBro(bootsharp.manifest.wasm) }); async function measure(dir) { let size = 0; @@ -25,7 +17,8 @@ async function measure(dir) { return Math.ceil(size / 1024); } -async function fetchBro(resource) { - const bro = await fs.readFile(`./cs/bin/bootsharp/bro/${resource.name}.br`); - resource.content = await util.promisify(zlib.brotliDecompress)(bro); +async function loadBro(name) { + const bro = await fs.readFile(`./cs/bin/bootsharp/bro/${name}.br`); + const buf = await util.promisify(zlib.brotliDecompress)(bro); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); } From a1b9a420322ae5cd51e7f959fe7ebb130f82b417 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 19:38:51 +0300 Subject: [PATCH 12/19] update trimming sample --- samples/react/backend/Backend.WASM/Backend.WASM.csproj | 2 -- samples/trimming/README.md | 6 +++--- samples/trimming/cs/Trimming.csproj | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/samples/react/backend/Backend.WASM/Backend.WASM.csproj b/samples/react/backend/Backend.WASM/Backend.WASM.csproj index f66badfd..5032bab0 100644 --- a/samples/react/backend/Backend.WASM/Backend.WASM.csproj +++ b/samples/react/backend/Backend.WASM/Backend.WASM.csproj @@ -8,8 +8,6 @@ backend $(SolutionDir) - - false $(SolutionDir)../public/bin diff --git a/samples/trimming/README.md b/samples/trimming/README.md index fae98506..f1f0d2a5 100644 --- a/samples/trimming/README.md +++ b/samples/trimming/README.md @@ -9,6 +9,6 @@ To test and measure build size: | Bootsharp | Raw | Brotli | |-------------|-------|--------| -| 0.1 .NET 8 | 2,298 | 739 | -| 0.7 .NET 9 | 1,737 | 518 | -| 0.8 .NET 10 | 1,610 | 482 | +| 0.1 .NET 8 | 2,010 | 662 | +| 0.7 .NET 9 | 1,449 | 441 | +| 0.8 .NET 10 | 1,322 | 405 | diff --git a/samples/trimming/cs/Trimming.csproj b/samples/trimming/cs/Trimming.csproj index 4df6acee..125cbed6 100644 --- a/samples/trimming/cs/Trimming.csproj +++ b/samples/trimming/cs/Trimming.csproj @@ -3,8 +3,6 @@ net10.0 browser-wasm - - false From 089870b3f4f131fb7863f06cdeafcc832dc38763 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 20:08:29 +0300 Subject: [PATCH 13/19] update docs --- docs/.vitepress/config.ts | 1 - docs/guide/build-config.md | 23 ++++++++++------------ docs/guide/getting-started.md | 15 ++++++++------- docs/guide/index.md | 2 +- docs/guide/sideloading.md | 36 ----------------------------------- docs/scripts/api.sh | 17 ++++++++++------- 6 files changed, 29 insertions(+), 65 deletions(-) delete mode 100644 docs/guide/sideloading.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 90764503..3965862c 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -60,7 +60,6 @@ export default defineConfig({ { text: "Interop Instances", link: "/guide/interop-instances" }, { text: "Preferences", link: "/guide/preferences" }, { text: "Build Configuration", link: "/guide/build-config" }, - { text: "Sideloading Binaries", link: "/guide/sideloading" }, { text: "NativeAOT-LLVM", link: "/guide/llvm" } ] }, diff --git a/docs/guide/build-config.md b/docs/guide/build-config.md index 3be3e741..0ee5e03c 100644 --- a/docs/guide/build-config.md +++ b/docs/guide/build-config.md @@ -2,17 +2,15 @@ Build and publish related options are configured in `.csproj` file via MSBuild properties. -| Property | Default | Description | -|----------------------------|------------|--------------------------------------------------------------| -| BootsharpName | bootsharp | Name of the generated JavaScript module. | -| BootsharpEmbedBinaries | true | Whether to embed binaries to the JavaScript module file. | -| BootsharpBundleCommand | npx rollup | The command to bundle generated JavaScrip solution. | -| BootsharpPublishDirectory | /bin | Directory to publish generated JavaScript module. | -| BootsharpTypesDirectory | /types | Directory to publish type declarations. | -| BootsharpBinariesDirectory | /bin | Directory to publish binaries when `EmbedBinaries` disabled. | -| BootsharpPackageDirectory | / | Directory to publish `package.json` file. | - -Below is an example configuration, which will make Bootsharp name compiled module "backend" (instead of the default "bootsharp"), publish the module under solution directory root (instead of "/bin") and disable binaries embedding in favor of publishing them under "public/bin" directory one level above the solution root: +| Property | Default | Description | +|----------------------------|------------------|---------------------------------------------------| +| BootsharpName | bootsharp | Name of the generated JavaScript module. | +| BootsharpPublishDirectory | /bin/bootsharp | Directory to publish generated JavaScript module. | +| BootsharpTypesDirectory | publish-dir/types| Directory to publish type declarations. | +| BootsharpBinariesDirectory | publish-dir/bin | Directory to publish binaries. | +| BootsharpPackageDirectory | project-dir | Directory to publish `package.json` file. | + +Below is an example configuration, which will make Bootsharp name the compiled module "backend" (instead of the default "bootsharp"), publish the `package.json` under the solution directory root and emit the runtime binaries into a "public/bin" directory one level above the solution root: ```xml @@ -22,7 +20,6 @@ Below is an example configuration, which will make Bootsharp name compiled modul browser-wasm backend $(SolutionDir) - false $(SolutionDir)../public/bin @@ -45,7 +42,7 @@ To enable globalization, explicitly disable invariant globalization in your proj ``` -When invariant globalization is disabled, Bootsharp will automatically include the ICU files emitted by the .NET WASM build and configure the runtime accordingly. This works for both embedded and sideloaded binaries. +When invariant globalization is disabled, Bootsharp will automatically include the ICU files emitted by the .NET WASM build and configure the runtime accordingly. Bootsharp supports the following globalization modes: diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 816f6b1d..42480f3b 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -58,13 +58,14 @@ Run following command under the solution root: dotnet publish ``` -— which will produce `bin/bootsharp` directory with the following content: +— which will produce a `bin/bootsharp` directory with the compiled module and a `package.json` next to the `.csproj`: -| Name | Type | Description | -|--------------|--------|-----------------------------------------------------------| -| types | folder | Contains type declarations for the authored interop APIs. | -| index.mjs | file | The compiled ES module with embedded binaries. | -| package.json | file | NPM package manifest for convenient importing. | +| Name | Type | Description | +|-----------|--------|----------------------------------------------------| +| bin | folder | Runtime binaries (`*.wasm`, assemblies, ICU data). | +| dotnet | folder | .NET runtime JavaScript modules. | +| types | folder | Type declarations for the authored interop APIs. | +| index.mjs | file | ES module entry point with the generated bindings. | ::: tip When publishing in `Release` (default for `dotnet publish`), Bootsharp automatically enables the [NativeAOT-LLVM](/guide/llvm) compiler, speed-focused WASM optimization, aggressive trimming, and an extra Binaryen pass when `wasm-opt` is available. @@ -110,7 +111,7 @@ console.log(`Hello ${Program.getBackendName()}!`); Program.onMainInvoked.subscribe(console.log); // Initializing dotnet runtime and invoking entry point. - await bootsharp.boot(); + await bootsharp.boot("/bin"); // Invoking 'Program.GetBackendName' C# method. console.log(`Hello ${Program.getBackendName()}!`); diff --git a/docs/guide/index.md b/docs/guide/index.md index d406b9fc..f47e01e3 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -43,7 +43,7 @@ Bootsharp will automatically build and bundle the JavaScript package when publis import bootsharp, { Backend, Frontend } from "backend"; // Boot C# WASM module. -await boosharp.boot(); +await bootsharp.boot("/bin"); // Subscribe to C# event. Frontend.onUserChanged.subscribe(updateUserUI); diff --git a/docs/guide/sideloading.md b/docs/guide/sideloading.md deleted file mode 100644 index 738209b0..00000000 --- a/docs/guide/sideloading.md +++ /dev/null @@ -1,36 +0,0 @@ -# Sideloading Binaries - -By default, Bootsharp build task will embed project's DLLs and .NET WASM runtime to the generated JavaScript module. While convenient and even required in some cases (eg, for VS Code web extensions), this also adds about 30% of extra size due to binary -> base64 conversion of the embedded files. - -To disable the embedding, set `BootsharpEmbedBinaries` build property to false: - -```xml - - - false - -``` - -The `dotnet.wasm` and solution's assemblies will be emitted in the build output directory. You will then have to provide them when booting: - -```ts -const resources = { - wasm: Uint8Array, - assemblies: [{ name: "Foo.wasm", content: Uint8Array }], - entryAssemblyName: "Foo.dll" -}; -await dotnet.boot({ resources }); -``` - -— this way the binary files can be streamed directly from server to optimize traffic and initial load time. - -Alternatively, set `root` property of the boot options and Bootsharp will automatically fetch the resources form the specified URL: - -```ts -// Assuming the resources are stored in "bin" directory under website root. -await backend.boot({ root: "/bin" }); -``` - -::: tip EXAMPLE -Find sideloading example in the [React sample](https://github.com/elringus/bootsharp/blob/main/samples/react). -::: diff --git a/docs/scripts/api.sh b/docs/scripts/api.sh index 3f3dce44..75b2efd5 100644 --- a/docs/scripts/api.sh +++ b/docs/scripts/api.sh @@ -1,11 +1,14 @@ # https://typedoc-plugin-markdown.org/themes/vitepress/quick-start +DOCS_DIR=$(cd "$(dirname "$0")/.." && pwd) +JS_DIR=$(cd "$DOCS_DIR/../src/js" && pwd) + echo '{ "entryPoints": [ - "../src/js/src/index.ts" + "src/index.mts" ], - "tsconfig": "../src/js/tsconfig.json", - "out": "api", + "tsconfig": "tsconfig.json", + "out": "../../docs/api", "name": "Bootsharp", "readme": "none", "githubPages": false, @@ -17,8 +20,8 @@ echo '{ "title.memberPage": "{name}", }, "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"] -}' > typedoc.json +}' > "$JS_DIR/typedoc.json" -typedoc -sed -i -z "s/API Reference/API Reference\nAuto-generated with [typedoc-plugin-markdown](https:\/\/typedoc-plugin-markdown.org)./" api/index.md -rm typedoc.json +(cd "$JS_DIR" && NODE_PATH="$DOCS_DIR/node_modules" "$DOCS_DIR/node_modules/.bin/typedoc" --skipErrorChecking) +sed -i -z "s/API Reference/API Reference\nAuto-generated with [typedoc-plugin-markdown](https:\/\/typedoc-plugin-markdown.org)./" "$DOCS_DIR/api/index.md" +rm "$JS_DIR/typedoc.json" From a43ba6c6395bbce7d986975857e3899dd1d3bf4f Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 20:08:58 +0300 Subject: [PATCH 14/19] qualify node modules --- samples/minimal/cs/package.json | 12 +++++------ .../Bootsharp.Publish/Pack/ModulePatcher.cs | 16 +++++++-------- src/cs/Bootsharp/Build/PackageTemplate.json | 20 +++++++++++-------- src/cs/Directory.Build.props | 2 +- src/js/test/cs/Test/package.json | 12 +++++++---- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/samples/minimal/cs/package.json b/samples/minimal/cs/package.json index 42e6b326..a75bba9c 100644 --- a/samples/minimal/cs/package.json +++ b/samples/minimal/cs/package.json @@ -4,11 +4,11 @@ "exports": "./bin/bootsharp/index.mjs", "types": "./bin/bootsharp/types/index.d.mts", "browser": { - "fs": false, - "url": false, - "path": false, - "module": false, - "crypto": false, - "process": false + "node:fs": false, + "node:url": false, + "node:path": false, + "node:module": false, + "node:crypto": false, + "node:process": false } } diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs index b3c5a05d..eb7f57e6 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs @@ -14,8 +14,8 @@ public void Patch () { RemoveMaps(); RemoveWasmNag(); - QualifyImports(); PacifyBundlers(); + QualifyImports(); } private void RemoveMaps () @@ -33,13 +33,6 @@ private void RemoveWasmNag () "true", dotnet); } - private void QualifyImports () - { - // Deno requires 'node:' prefix on Node built-ins. - Rewrite(new(@"import\(([^""')]*)['""](?!node:)(url|fs|process|module|path|crypto)['""]\)", CultureInvariant), - "import($1\"node:$2\")", dotnet, runtime, native); - } - private void PacifyBundlers () { // Neutralizes the bundler-offending code in the .NET and Emscripten's generated ES modules. @@ -50,6 +43,13 @@ private void PacifyBundlers () "\"file:///c:/x.js\"", dotnet, runtime, native); } + private void QualifyImports () + { + // Deno requires 'node:' prefix on Node built-ins. + Rewrite(new(@"import\(([^""')]*)['""](?!node:)(url|fs|process|module|path|crypto)['""]\)", CultureInvariant), + "import($1\"node:$2\")", dotnet, runtime, native); + } + private static void Rewrite (Regex pattern, string with, params ReadOnlySpan paths) { foreach (var path in paths) diff --git a/src/cs/Bootsharp/Build/PackageTemplate.json b/src/cs/Bootsharp/Build/PackageTemplate.json index e6f52c25..c4d49f4e 100644 --- a/src/cs/Bootsharp/Build/PackageTemplate.json +++ b/src/cs/Bootsharp/Build/PackageTemplate.json @@ -1,14 +1,18 @@ { "name": "%MODULE_NAME%", "type": "module", - "exports": "./%MODULE_DIR%/index.mjs", - "types": "./%TYPES_DIR%/index.d.mts", + "exports": { + ".": { + "types": "./%TYPES_DIR%/index.d.mts", + "default": "./%MODULE_DIR%/index.mjs" + } + }, "browser": { - "fs": false, - "url": false, - "path": false, - "module": false, - "crypto": false, - "process": false + "node:fs": false, + "node:url": false, + "node:path": false, + "node:module": false, + "node:crypto": false, + "node:process": false } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 1561205f..30e7e791 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.217 + 0.8.0-alpha.220 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test/package.json b/src/js/test/cs/Test/package.json index 1753e5b2..a75bba9c 100644 --- a/src/js/test/cs/Test/package.json +++ b/src/js/test/cs/Test/package.json @@ -1,10 +1,14 @@ { "name": "bootsharp", "type": "module", - "main": "bin/bootsharp/index.mjs", - "types": "bin/bootsharp/types/index.d.mts", + "exports": "./bin/bootsharp/index.mjs", + "types": "./bin/bootsharp/types/index.d.mts", "browser": { - "url": false, "fs": false, "process": false, - "module": false, "path": false, "crypto": false + "node:fs": false, + "node:url": false, + "node:path": false, + "node:module": false, + "node:crypto": false, + "node:process": false } } From 8af1a73a85e49194aafd5101e67c24981eb32ef1 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 21:05:02 +0300 Subject: [PATCH 15/19] flatten types --- docs/guide/build-config.md | 1 - src/cs/Bootsharp/Build/Bootsharp.props | 2 -- src/cs/Bootsharp/Build/Bootsharp.targets | 6 ++---- src/cs/Bootsharp/Build/PackageTemplate.json | 5 +---- src/cs/Directory.Build.props | 2 +- src/js/test/cs.ts | 7 +++---- src/js/test/cs/Test/package.json | 5 +++-- src/js/test/spec/boot.spec.ts | 2 +- 8 files changed, 11 insertions(+), 19 deletions(-) diff --git a/docs/guide/build-config.md b/docs/guide/build-config.md index 0ee5e03c..222dfb79 100644 --- a/docs/guide/build-config.md +++ b/docs/guide/build-config.md @@ -6,7 +6,6 @@ Build and publish related options are configured in `.csproj` file via MSBuild p |----------------------------|------------------|---------------------------------------------------| | BootsharpName | bootsharp | Name of the generated JavaScript module. | | BootsharpPublishDirectory | /bin/bootsharp | Directory to publish generated JavaScript module. | -| BootsharpTypesDirectory | publish-dir/types| Directory to publish type declarations. | | BootsharpBinariesDirectory | publish-dir/bin | Directory to publish binaries. | | BootsharpPackageDirectory | project-dir | Directory to publish `package.json` file. | diff --git a/src/cs/Bootsharp/Build/Bootsharp.props b/src/cs/Bootsharp/Build/Bootsharp.props index f6b98a7c..5ffb38cc 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.props +++ b/src/cs/Bootsharp/Build/Bootsharp.props @@ -19,8 +19,6 @@ bootsharp - - diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 85e7a710..d189e3d7 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -90,7 +90,6 @@ $(WasmAppDir)/$(WasmRuntimeAssetsLocation) $(BaseOutputPath.Replace('\', '/')) $(BsBaseOutputPath)$(BootsharpName) - $(BootsharpPublishDirectory)/types $(BootsharpPublishDirectory)/bin $(MSBuildProjectDirectory) $([System.String]::Equals('$(InvariantGlobalization)', 'false')) @@ -133,13 +132,13 @@ - + - + @@ -160,7 +159,6 @@ Lines="$([System.IO.File]::ReadAllText('$(BsRoot)/build/PackageTemplate.json') .Replace('%MODULE_NAME%','$(BootsharpName)') .Replace('%MODULE_DIR%','$([System.IO.Path]::GetRelativePath('$(BootsharpPackageDirectory)','$(BootsharpPublishDirectory)'))') - .Replace('%TYPES_DIR%','$([System.IO.Path]::GetRelativePath('$(BootsharpPackageDirectory)','$(BootsharpTypesDirectory)'))') .Replace('\', '/'))"/> diff --git a/src/cs/Bootsharp/Build/PackageTemplate.json b/src/cs/Bootsharp/Build/PackageTemplate.json index c4d49f4e..be336b46 100644 --- a/src/cs/Bootsharp/Build/PackageTemplate.json +++ b/src/cs/Bootsharp/Build/PackageTemplate.json @@ -2,10 +2,7 @@ "name": "%MODULE_NAME%", "type": "module", "exports": { - ".": { - "types": "./%TYPES_DIR%/index.d.mts", - "default": "./%MODULE_DIR%/index.mjs" - } + ".": "./%MODULE_DIR%/index.mjs" }, "browser": { "node:fs": false, diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 30e7e791..6e13d463 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.220 + 0.8.0-alpha.221 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs.ts b/src/js/test/cs.ts index 864d1ca6..87940193 100644 --- a/src/js/test/cs.ts +++ b/src/js/test/cs.ts @@ -1,11 +1,10 @@ import assert from "node:assert"; import { resolve } from "node:path"; import { readFileSync, existsSync } from "node:fs"; -import type { BootResources, BinaryResource } from "./cs/Test"; -import bootsharp, { Test } from "./cs/Test"; +import bootsharp, { Test, BootResources, BinaryResource } from "./cs/Test/bin/bootsharp/index.mjs"; export { bootsharp, Test }; -export * from "./cs/Test"; +export * from "./cs/Test/bin/bootsharp/index.mjs"; export const resources = loadResources(); export const manifest = bootsharp.manifest; @@ -16,7 +15,7 @@ export async function bootRuntime() { } export function getDeclarations() { - const file = resolvePath("test/cs/Test/bin/bootsharp/types/generated/bindings.g.d.mts"); + const file = resolvePath("test/cs/Test/bin/bootsharp/generated/bindings.g.d.mts"); return readFileSync(file).toString(); } diff --git a/src/js/test/cs/Test/package.json b/src/js/test/cs/Test/package.json index a75bba9c..2c5ec2ab 100644 --- a/src/js/test/cs/Test/package.json +++ b/src/js/test/cs/Test/package.json @@ -1,8 +1,9 @@ { "name": "bootsharp", "type": "module", - "exports": "./bin/bootsharp/index.mjs", - "types": "./bin/bootsharp/types/index.d.mts", + "exports": { + ".": "./bin/bootsharp/index.mjs" + }, "browser": { "node:fs": false, "node:url": false, diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 20a65478..af27c680 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -1,7 +1,7 @@ import { resolve } from "node:path"; import { readFileSync } from "node:fs"; import { describe, expect, it, vi } from "vitest"; -import type { BootOptions } from "../cs/Test"; +import type { BootOptions } from "../cs/Test/bin/bootsharp/index.mjs"; async function setup() { vi.resetModules(); From c3d3ca04f63281e1ca8b884038ffaa4ed57e64b7 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 21:11:34 +0300 Subject: [PATCH 16/19] etc --- samples/bench/bootsharp/package.json | 17 +++++++++-------- src/cs/Directory.Build.props | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/samples/bench/bootsharp/package.json b/samples/bench/bootsharp/package.json index 42e6b326..2c5ec2ab 100644 --- a/samples/bench/bootsharp/package.json +++ b/samples/bench/bootsharp/package.json @@ -1,14 +1,15 @@ { "name": "bootsharp", "type": "module", - "exports": "./bin/bootsharp/index.mjs", - "types": "./bin/bootsharp/types/index.d.mts", + "exports": { + ".": "./bin/bootsharp/index.mjs" + }, "browser": { - "fs": false, - "url": false, - "path": false, - "module": false, - "crypto": false, - "process": false + "node:fs": false, + "node:url": false, + "node:path": false, + "node:module": false, + "node:crypto": false, + "node:process": false } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 6e13d463..4ab8c8fc 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.221 + 0.8.0-alpha.222 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 87256b0b09035f03addfee286484216f4840e37d Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 21:22:46 +0300 Subject: [PATCH 17/19] etc --- src/cs/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 4ab8c8fc..d9e9f3c1 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.222 + 0.8.0-alpha.224 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 00eea3b1876034ca435f7218ab49031282c6da9a Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 21:28:04 +0300 Subject: [PATCH 18/19] update docs --- docs/guide/getting-started.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 42480f3b..f87d1ba6 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -58,14 +58,7 @@ Run following command under the solution root: dotnet publish ``` -— which will produce a `bin/bootsharp` directory with the compiled module and a `package.json` next to the `.csproj`: - -| Name | Type | Description | -|-----------|--------|----------------------------------------------------| -| bin | folder | Runtime binaries (`*.wasm`, assemblies, ICU data). | -| dotnet | folder | .NET runtime JavaScript modules. | -| types | folder | Type declarations for the authored interop APIs. | -| index.mjs | file | ES module entry point with the generated bindings. | +— which will produce a `bin/bootsharp` directory with the compiled module and a `package.json` next to the `.csproj`. ::: tip When publishing in `Release` (default for `dotnet publish`), Bootsharp automatically enables the [NativeAOT-LLVM](/guide/llvm) compiler, speed-focused WASM optimization, aggressive trimming, and an extra Binaryen pass when `wasm-opt` is available. From 40275fdb1d6d64f45a4583c5b93cba6692953e0b Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 10 May 2026 21:30:24 +0300 Subject: [PATCH 19/19] etc --- samples/minimal/cs/package.json | 5 +++-- samples/trimming/cs/package.json | 17 +++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/samples/minimal/cs/package.json b/samples/minimal/cs/package.json index a75bba9c..2c5ec2ab 100644 --- a/samples/minimal/cs/package.json +++ b/samples/minimal/cs/package.json @@ -1,8 +1,9 @@ { "name": "bootsharp", "type": "module", - "exports": "./bin/bootsharp/index.mjs", - "types": "./bin/bootsharp/types/index.d.mts", + "exports": { + ".": "./bin/bootsharp/index.mjs" + }, "browser": { "node:fs": false, "node:url": false, diff --git a/samples/trimming/cs/package.json b/samples/trimming/cs/package.json index 42e6b326..2c5ec2ab 100644 --- a/samples/trimming/cs/package.json +++ b/samples/trimming/cs/package.json @@ -1,14 +1,15 @@ { "name": "bootsharp", "type": "module", - "exports": "./bin/bootsharp/index.mjs", - "types": "./bin/bootsharp/types/index.d.mts", + "exports": { + ".": "./bin/bootsharp/index.mjs" + }, "browser": { - "fs": false, - "url": false, - "path": false, - "module": false, - "crypto": false, - "process": false + "node:fs": false, + "node:url": false, + "node:path": false, + "node:module": false, + "node:crypto": false, + "node:process": false } }