Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
dotnet-version: 10
- uses: actions/setup-node@v6
with:
node-version: 24
node-version: 26
- name: cover
run: |
cd src/js
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default defineConfig({
{ text: "Interop Instances", link: "/guide/interop-instances" },
{ text: "Preferences", link: "/guide/preferences" },
{ text: "Build Configuration", link: "/guide/build-config" },
{ text: "Sideloading", link: "/guide/sideloading" },
{ text: "NativeAOT-LLVM", link: "/guide/llvm" }
]
},
Expand Down
12 changes: 6 additions & 6 deletions docs/guide/build-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

Build and publish related options are configured in `.csproj` file via MSBuild properties.

| Property | Default | Description |
|----------------------------|------------------|---------------------------------------------------|
| BootsharpName | bootsharp | Name of the generated JavaScript module. |
| BootsharpPublishDirectory | /bin/bootsharp | Directory to publish generated JavaScript module. |
| BootsharpBinariesDirectory | publish-dir/bin | Directory to publish binaries. |
| BootsharpPackageDirectory | project-dir | Directory to publish `package.json` file. |
| Property | Default | Description |
|----------------------------|------------------|-----------------------------------------------------------------------------------|
| BootsharpName | bootsharp | Name of the generated JavaScript module. |
| BootsharpPublishDirectory | /bin/bootsharp | Directory to publish generated JavaScript module. |
| BootsharpBinariesDirectory | (empty) | Directory to publish binaries; when empty, binaries are embedded (see [Sideloading](sideloading)). |
| 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:

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ console.log(`Hello ${Program.getBackendName()}!`);
Program.onMainInvoked.subscribe(console.log);

// Initializing dotnet runtime and invoking entry point.
await bootsharp.boot("/bin");
await bootsharp.boot();

// Invoking 'Program.GetBackendName' C# method.
console.log(`Hello ${Program.getBackendName()}!`);
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 bootsharp.boot("/bin");
await bootsharp.boot();

// Subscribe to C# event.
Frontend.onUserChanged.subscribe(updateUserUI);
Expand Down
35 changes: 35 additions & 0 deletions docs/guide/sideloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Sideloading Binaries

By default, Bootsharp embeds the .NET WASM runtime and the solution's assemblies into the generated JavaScript module as base64 strings. This is convenient — `bootsharp.boot()` works with no arguments and no extra files to serve — and required in some environments (eg, VS Code web extensions). The trade-off is roughly 30% extra bundle size due to the base64 encoding.

To disable embedding, set `BootsharpBinariesDirectory` to the directory where the binaries should be published:

```xml
<PropertyGroup>
<BootsharpBinariesDirectory>bin/$(BootsharpName)/bin</BootsharpBinariesDirectory>
</PropertyGroup>
```

The `dotnet.native.wasm`, solution assemblies, ICU data and (in debug builds) debug symbols will be emitted to that directory as separate files instead of being inlined into the module. You then have two ways to feed them to `boot`:

Pass a root URL to fetch the resources from at runtime:

```ts
// Assuming the binaries are served from "/bin" under the website root.
await bootsharp.boot("/bin");
```

Or load the binaries yourself and pass them as a `BootResources` object:

```ts
import { readFileSync } from "node:fs";

const wasm = readFileSync("bin/dotnet.native.wasm");
await bootsharp.boot({ wasm });
```

This way the binary files can be streamed directly from the server, cached separately, or loaded from any source you control — useful for trimming initial bundle size or sharing the runtime across multiple modules.

::: tip EXAMPLE
Find sideloading example in the [minimal sample](https://github.com/elringus/bootsharp/blob/main/samples/minimal).
:::
2 changes: 1 addition & 1 deletion samples/bench/bootsharp/init.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export async function init() {
IImported.getNumber = getNumber;
IImported.getStruct = getStruct;

await bootsharp.boot(import.meta.resolve("./bin/bootsharp/bin"));
await bootsharp.boot();

return { ...IExported };
}
2 changes: 1 addition & 1 deletion samples/minimal/README.md
Original file line number Diff line number Diff line change
@@ -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) (blocker: https://github.com/elringus/bootsharp/pull/203);
- Run `node main.mjs` to test in [Node](https://nodejs.org);
- 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.
2 changes: 1 addition & 1 deletion samples/minimal/cs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"exports": {
".": "./bin/bootsharp/index.mjs",
"./*": "./bin/bootsharp/generated/*.g.mjs"
"./*": "./bin/bootsharp/generated/modules/*.g.mjs"
},
"browser": {
"node:fs": false,
Expand Down
2 changes: 1 addition & 1 deletion samples/minimal/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h1 id="msg">Loading...</h1>
Program.onMainInvoked.subscribe(console.log);

// Initializing dotnet runtime and invoking entry point.
await bootsharp.boot("/cs/bin/bootsharp/bin");
await bootsharp.boot();

// Invoking 'Program.GetBackendName' C# method.
document.getElementById("msg").innerHTML = `Hello ${Program.getBackendName()}!`;
Expand Down
2 changes: 1 addition & 1 deletion samples/minimal/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Program.getFrontendName = () =>
Program.onMainInvoked.subscribe(console.log);

// Initializing dotnet runtime and invoking entry point.
await bootsharp.boot(import.meta.resolve("./cs/bin/bootsharp/bin"));
await bootsharp.boot();

// Invoking 'Program.GetBackendName' C# method.
console.log(`Hello ${Program.getBackendName()}!`);
2 changes: 2 additions & 0 deletions samples/trimming/cs/Trimming.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<!-- Disabling embedded mode reduces size by ~30% -->
<BootsharpBinariesDirectory>bin/bootsharp/bin</BootsharpBinariesDirectory>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/trimming/cs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"exports": {
".": "./bin/bootsharp/index.mjs",
"./*": "./bin/bootsharp/generated/*.g.mjs"
"./*": "./bin/bootsharp/generated/modules/*.g.mjs"
},
"browser": {
"node:fs": false,
Expand Down
3 changes: 2 additions & 1 deletion src/cs/Bootsharp.Publish.Test/GenerateJS/GenerateJSTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ protected override void AddAssembly (string assemblyName, params MockSource[] so
BuildEngine = Engine,
Globalization = false,
LLVM = false,
Debug = false
Debug = false,
Embed = false
};
}
19 changes: 19 additions & 0 deletions src/cs/Bootsharp.Publish.Test/GenerateJS/ResourceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,23 @@ public void WhenGlobalizationDisabledIcuNotIncluded ()
Execute();
DoesNotContain("icudt.dat");
}

[Fact]
public void WhenEmbedEnabledBinaryContentEmbeddedAsBase64 ()
{
Task.Embed = true;
Task.Globalization = true;
Task.Debug = true;
AddAssembly("Foo.dll");
Project.WriteFile("Foo.wasm", "MockFooContent"u8);
Project.WriteFile("icudt.dat", "MockIcuContent"u8);
Project.WriteFile("Foo.pdb", "MockPdbContent"u8);
Project.WriteFile("dotnet.native.js.symbols", "MockSymbolsContent"u8);
Execute();
Contains($"wasm: \"{Convert.ToBase64String(MockWasmBinary)}\"");
Contains($"{{ name: \"Foo.wasm\", content: \"{Convert.ToBase64String("MockFooContent"u8)}\" }}");
Contains($"{{ name: \"icudt.dat\", content: \"{Convert.ToBase64String("MockIcuContent"u8)}\" }}");
Contains($"{{ name: \"Foo.pdb\", content: \"{Convert.ToBase64String("MockPdbContent"u8)}\" }}");
Contains($"{{ name: \"dotnet.native.js.symbols\", content: \"{Convert.ToBase64String("MockSymbolsContent"u8)}\" }}");
}
}
3 changes: 2 additions & 1 deletion src/cs/Bootsharp.Publish/GenerateJS/GenerateJS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public sealed class GenerateJS : Microsoft.Build.Utilities.Task
public required bool Globalization { get; set; }
public required bool LLVM { get; set; }
public required bool Debug { get; set; }
public required bool Embed { get; set; }

public override bool Execute ()
{
Expand Down Expand Up @@ -83,7 +84,7 @@ private void GenerateDeclarations (SolutionInspection spec, JSModules mds)

private void GenerateResources (SolutionInspection spec)
{
var generator = new ResourceGenerator(EntryAssemblyName, Debug, Globalization);
var generator = new ResourceGenerator(EntryAssemblyName, Debug, Globalization, Embed);
var content = generator.Generate(BuildDirectory, DebugDirectory);
WriteGenerated("resources.g.mjs", content);
}
Expand Down
86 changes: 54 additions & 32 deletions src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
namespace Bootsharp.Publish;

/// <summary>
/// Generates a manifest listing resources required to initialize the .NET runtime.
/// Generates a manifest listing resources required to initialize the .NET runtime,
/// optionally embedding the binary content as base64 strings.
/// </summary>
internal sealed class ResourceGenerator (string entryAssemblyName, bool debug, bool g11n)
internal sealed class ResourceGenerator (string entryAssemblyName, bool debug, bool g11n, bool embed)
{
private readonly List<string> assemblies = [];
private readonly List<string> symbols = [];
Expand All @@ -14,43 +15,64 @@ 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 = BuildResourceName(path);
else assemblies.Add(BuildResourceName(path));
if (path.EndsWith("dotnet.native.wasm")) wasm = Path.GetFileName(path);
else assemblies.Add(Path.GetFileName(path));
if (g11n)
{
foreach (var path in Directory.GetFiles(buildDir, "*.dat").Order())
icu.Add(BuildResourceName(path));
}
icu.Add(Path.GetFileName(path));
if (debug)
{
foreach (var path in Directory.GetFiles(debugDir, "*.symbols").Order())
symbols.Add(BuildResourceName(path));
symbols.Add(Path.GetFileName(path));
foreach (var path in Directory.GetFiles(debugDir, "*.pdb").Order())
pdb.Add(BuildResourceName(path));
pdb.Add(Path.GetFileName(path));
}
return
$$"""
export default {
wasm: {{wasm}},
assemblies: [
{{Fmt(assemblies, 2, ",\n")}}
],
icu: [
{{Fmt(icu, 2, ",\n")}}
],
symbols: [
{{Fmt(symbols, 2, ",\n")}}
],
pdb: [
{{Fmt(pdb, 2, ",\n")}}
],
entryAssemblyName: "{{entryAssemblyName}}"
};
""";
return $"{GenerateManifest()}\n\n{GenerateEmbedded(buildDir, debugDir)}";
}

private string BuildResourceName (string path)
{
return $"\"{Path.GetFileName(path)}\"";
}
private string GenerateManifest () =>
$$"""
export const manifest = {
wasm: "{{wasm}}",
assemblies: [
{{FmtNames(assemblies)}}
],
icu: [
{{FmtNames(icu)}}
],
symbols: [
{{FmtNames(symbols)}}
],
pdb: [
{{FmtNames(pdb)}}
],
entryAssemblyName: "{{entryAssemblyName}}"
};
""";

private string GenerateEmbedded (string buildDir, string debugDir) => embed ?
$$"""
export const embedded = {
wasm: "{{ReadBase64(buildDir, wasm)}}",
assemblies: [
{{FmtBins(buildDir, assemblies)}}
],
icu: [
{{FmtBins(buildDir, icu)}}
],
symbols: [
{{FmtBins(debugDir, symbols)}}
],
pdb: [
{{FmtBins(debugDir, pdb)}}
]
};
""" : "export const embedded = undefined;";

private static string FmtNames (IEnumerable<string> names) =>
Fmt(names.Select(n => $"\"{n}\""), 2, ",\n");
private static string FmtBins (string dir, IEnumerable<string> names) =>
Fmt(names.Select(n => $"{{ name: \"{n}\", content: \"{ReadBase64(dir, n)}\" }}"), 2, ",\n");
private static string ReadBase64 (string dir, string name) =>
Convert.ToBase64String(File.ReadAllBytes(Path.Combine(dir, name)));
}
4 changes: 2 additions & 2 deletions src/cs/Bootsharp/Build/Bootsharp.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
<!-- User Preferences -->
<!-- Name of the generated JavaScript module; 'bootsharp' by default. -->
<BootsharpName>bootsharp</BootsharpName>
<!-- Directory to publish binaries; when not specified, will publish in embedded mode. -->
<BootsharpBinariesDirectory/>
<!-- Directory to publish generated JavaScript module; 'base-output/module-name' by default. -->
<BootsharpPublishDirectory/>
<!-- Directory to publish binaries; 'publish-dir/bin' by default. -->
<BootsharpBinariesDirectory/>
<!-- Directory to publish 'package.json' file; equals project directoy by default. -->
<BootsharpPackageDirectory/>

Expand Down
13 changes: 7 additions & 6 deletions src/cs/Bootsharp/Build/Bootsharp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<BsEntryAssembly>$(AssemblyName).dll</BsEntryAssembly>
<BsPackAfter Condition="$(BsLlvm)">CopyNativeBinary</BsPackAfter>
<BsPackAfter Condition="!$(BsLlvm)">WasmNestedPublishApp</BsPackAfter>
<BsEmbed>$([System.String]::IsNullOrEmpty('$(BootsharpBinariesDirectory)'))</BsEmbed>
</PropertyGroup>

<!-- Include NativeAOT-LLVM compiler infrastructure when enabled. -->
Expand Down Expand Up @@ -90,7 +91,6 @@
<BsBuildDir Condition="!$(BsLlvm)">$(WasmAppDir)/$(WasmRuntimeAssetsLocation)</BsBuildDir>
<BsBaseOutputPath>$(BaseOutputPath.Replace('\', '/'))</BsBaseOutputPath>
<BootsharpPublishDirectory Condition="'$(BootsharpPublishDirectory)' == ''">$(BsBaseOutputPath)$(BootsharpName)</BootsharpPublishDirectory>
<BootsharpBinariesDirectory Condition="'$(BootsharpBinariesDirectory)' == ''">$(BootsharpPublishDirectory)/bin</BootsharpBinariesDirectory>
<BootsharpPackageDirectory Condition="'$(BootsharpPackageDirectory)' == ''">$(MSBuildProjectDirectory)</BootsharpPackageDirectory>
<BsGlobalization>$([System.String]::Equals('$(InvariantGlobalization)', 'false'))</BsGlobalization>
</PropertyGroup>
Expand Down Expand Up @@ -122,7 +122,8 @@
EntryAssemblyName="$(BsEntryAssembly)"
Globalization="$(BsGlobalization)"
LLVM="$(BsLlvm)"
Debug="$(BsDebug)"/>
Debug="$(BsDebug)"
Embed="$(BsEmbed)"/>

<!-- Publish modules, binaries and type declarations. -->
<ItemGroup>
Expand All @@ -137,8 +138,8 @@
</ItemGroup>
<RemoveDir Directories="$(BootsharpBinariesDirectory)"/>
<RemoveDir Directories="$(BootsharpPublishDirectory)"/>
<Copy SourceFiles="@(BsWasmFiles)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy SourceFiles="@(BsIcuFiles)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy Condition="!$(BsEmbed)" SourceFiles="@(BsWasmFiles)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy Condition="!$(BsEmbed)" SourceFiles="@(BsIcuFiles)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy SourceFiles="@(BsDotNetFile)" DestinationFolder="$(BootsharpPublishDirectory)/dotnet"/>
<Copy SourceFiles="@(BsRuntimeFile)" DestinationFolder="$(BootsharpPublishDirectory)/dotnet"/>
<Copy SourceFiles="@(BsNativeFile)" DestinationFolder="$(BootsharpPublishDirectory)/dotnet"/>
Expand All @@ -152,8 +153,8 @@
<BsPdbs Include="$(PublishDir)/*.pdb"/>
</ItemGroup>
<Copy Condition="$(BsDebug)" SourceFiles="@(BsMaps)" DestinationFolder="$(BootsharpPublishDirectory)"/>
<Copy Condition="$(BsDebug)" SourceFiles="@(BsSymbols)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy Condition="$(BsDebug)" SourceFiles="@(BsPdbs)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy Condition="$(BsDebug) And !$(BsEmbed)" SourceFiles="@(BsSymbols)" DestinationFolder="$(BootsharpBinariesDirectory)"/>
<Copy Condition="$(BsDebug) And !$(BsEmbed)" SourceFiles="@(BsPdbs)" DestinationFolder="$(BootsharpBinariesDirectory)"/>

<!-- Publish package file. -->
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/cs/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<Version>0.8.0-alpha.353</Version>
<Version>0.8.0-alpha.361</Version>
<Authors>Elringus</Authors>
<PackageTags>javascript typescript ts js wasm node deno bun interop codegen</PackageTags>
<PackageProjectUrl>https://bootsharp.com</PackageProjectUrl>
Expand Down
8 changes: 4 additions & 4 deletions src/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
},
"devDependencies": {
"typescript": "6.0.3",
"@types/node": "25.6.2",
"@types/node": "25.9.1",
"@types/ws": "8.18.1",
"vitest": "4.1.5",
"@vitest/coverage-v8": "4.1.5",
"ws": "8.20.0"
"vitest": "4.1.7",
"@vitest/coverage-v8": "4.1.7",
"ws": "8.21.0"
}
}
Loading
Loading