├── .editorconfig
├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── codeql.yml
│ ├── docs.yml
│ ├── lint.yml
│ ├── package.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── docs
├── .gitignore
├── .vitepress
│ ├── config.ts
│ └── theme
│ │ ├── index.ts
│ │ └── style.css
├── guide
│ ├── build-config.md
│ ├── declarations.md
│ ├── emit-prefs.md
│ ├── events.md
│ ├── extensions
│ │ ├── dependency-injection.md
│ │ └── file-system.md
│ ├── getting-started.md
│ ├── index.md
│ ├── interop-instances.md
│ ├── interop-interfaces.md
│ ├── llvm.md
│ ├── namespaces.md
│ ├── serialization.md
│ └── sideloading.md
├── index.md
├── package.json
├── public
│ ├── favicon.svg
│ ├── fonts
│ │ ├── inter.woff2
│ │ └── jb.woff2
│ ├── img
│ │ ├── banner.png
│ │ ├── llvm-bench.png
│ │ ├── logo.png
│ │ └── og.jpg
│ └── imgit
│ │ ├── covers.json
│ │ ├── encoded
│ │ ├── img-banner.png@cover.avif
│ │ ├── img-banner.png@main.avif
│ │ ├── img-llvm-bench.png@cover.avif
│ │ ├── img-llvm-bench.png@dense.avif
│ │ └── img-llvm-bench.png@main.avif
│ │ ├── probes.json
│ │ ├── sizes.json
│ │ └── specs.json
├── scripts
│ └── api.sh
└── tsconfig.json
├── samples
├── bench
│ ├── bench.mjs
│ ├── bootsharp
│ │ ├── Boot.csproj
│ │ ├── Program.cs
│ │ ├── init.mjs
│ │ └── readme.md
│ ├── dotnet-llvm
│ │ ├── DotNetLLVM.csproj
│ │ ├── Program.cs
│ │ ├── imports.js
│ │ ├── init.mjs
│ │ └── readme.md
│ ├── dotnet
│ │ ├── DotNet.csproj
│ │ ├── Program.cs
│ │ ├── init.mjs
│ │ └── readme.md
│ ├── fixtures.mjs
│ ├── go
│ │ ├── .gitignore
│ │ ├── init.mjs
│ │ ├── main.go
│ │ └── readme.md
│ ├── readme.md
│ ├── rust
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── init.mjs
│ │ ├── readme.md
│ │ └── src
│ │ │ └── lib.rs
│ └── zig
│ │ ├── .gitignore
│ │ ├── build.zig
│ │ ├── init.mjs
│ │ ├── main.zig
│ │ └── readme.md
├── minimal
│ ├── README.md
│ ├── cs
│ │ ├── Minimal.csproj
│ │ └── Program.cs
│ ├── index.html
│ └── main.mjs
├── react
│ ├── README.md
│ ├── backend
│ │ ├── Backend.Prime
│ │ │ ├── Backend.Prime.csproj
│ │ │ ├── IPrimeUI.cs
│ │ │ ├── Options.cs
│ │ │ └── Prime.cs
│ │ ├── Backend.WASM
│ │ │ ├── Backend.WASM.csproj
│ │ │ └── Program.cs
│ │ ├── Backend.sln
│ │ ├── Backend
│ │ │ ├── Backend.csproj
│ │ │ └── IComputer.cs
│ │ └── package.json
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── favicon.svg
│ ├── src
│ │ ├── computer.tsx
│ │ ├── donut.tsx
│ │ ├── index.css
│ │ └── main.tsx
│ ├── test
│ │ ├── computer.test.tsx
│ │ └── setup.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── trimming
│ ├── README.md
│ ├── cs
│ │ ├── Program.cs
│ │ └── Trimming.csproj
│ └── main.mjs
└── vscode
│ ├── LICENSE.md
│ ├── README.md
│ ├── assets
│ └── package-icon.png
│ ├── package.json
│ └── src
│ └── extension.js
└── src
├── cs
├── .scripts
│ ├── cover.ps1
│ └── pack.ps1
├── Bootsharp.Common.Test
│ ├── Bootsharp.Common.Test.csproj
│ ├── InstancesTest.cs
│ ├── InterfacesTest.cs
│ ├── Mocks.cs
│ ├── ProxiesTest.cs
│ ├── SerializerTest.cs
│ └── TypesTest.cs
├── Bootsharp.Common
│ ├── Attributes
│ │ ├── JSEventAttribute.cs
│ │ ├── JSExportAttribute.cs
│ │ ├── JSFunctionAttribute.cs
│ │ ├── JSImportAttribute.cs
│ │ ├── JSInvokableAttribute.cs
│ │ └── JSPreferencesAttribute.cs
│ ├── Bootsharp.Common.csproj
│ ├── Error.cs
│ └── Interop
│ │ ├── ExportInterface.cs
│ │ ├── ImportInterface.cs
│ │ ├── Instances.cs
│ │ ├── Interfaces.cs
│ │ ├── Proxies.cs
│ │ └── Serializer.cs
├── Bootsharp.Generate.Test
│ ├── Bootsharp.Generate.Test.csproj
│ ├── EventTest.cs
│ ├── FunctionTest.cs
│ ├── GeneratorTest.cs
│ └── Verifier.cs
├── Bootsharp.Generate
│ ├── Bootsharp.Generate.csproj
│ ├── PartialClass.cs
│ ├── PartialMethod.cs
│ ├── SourceGenerator.cs
│ └── SyntaxReceiver.cs
├── Bootsharp.Inject.Test
│ ├── Bootsharp.Inject.Test.csproj
│ ├── ExtensionsTest.cs
│ └── Mocks.cs
├── Bootsharp.Inject
│ ├── Bootsharp.Inject.csproj
│ └── Extensions.cs
├── Bootsharp.Publish.Test
│ ├── Bootsharp.Publish.Test.csproj
│ ├── Emit
│ │ ├── DependenciesTest.cs
│ │ ├── EmitTest.cs
│ │ ├── InterfacesTest.cs
│ │ ├── InteropTest.cs
│ │ └── SerializerTest.cs
│ ├── Mock
│ │ ├── MockAssembly.cs
│ │ ├── MockCompiler.cs
│ │ ├── MockProject.cs
│ │ ├── MockSource.cs
│ │ └── MockTest.cs
│ ├── Pack
│ │ ├── BindingTest.cs
│ │ ├── DeclarationTest.cs
│ │ ├── PackTest.cs
│ │ ├── PatcherTest.cs
│ │ ├── ResourceTest.cs
│ │ └── SolutionInspectionTest.cs
│ ├── Packer.Test.csproj
│ └── TaskTest.cs
├── Bootsharp.Publish
│ ├── Bootsharp.Publish.csproj
│ ├── Common
│ │ ├── Global
│ │ │ ├── GlobalInspection.cs
│ │ │ ├── GlobalSerialization.cs
│ │ │ ├── GlobalText.cs
│ │ │ └── GlobalType.cs
│ │ ├── Meta
│ │ │ ├── ArgumentMeta.cs
│ │ │ ├── InterfaceKind.cs
│ │ │ ├── InterfaceMeta.cs
│ │ │ ├── MethodKind.cs
│ │ │ ├── MethodMeta.cs
│ │ │ └── ValueMeta.cs
│ │ ├── Preferences
│ │ │ ├── Preference.cs
│ │ │ ├── Preferences.cs
│ │ │ └── PreferencesResolver.cs
│ │ ├── SolutionInspector
│ │ │ ├── InspectionReporter.cs
│ │ │ ├── InterfaceInspector.cs
│ │ │ ├── MethodInspector.cs
│ │ │ ├── SolutionInspection.cs
│ │ │ └── SolutionInspector.cs
│ │ └── TypeConverter
│ │ │ ├── TypeConverter.cs
│ │ │ └── TypeCrawler.cs
│ ├── Emit
│ │ ├── BootsharpEmit.cs
│ │ ├── DependencyGenerator.cs
│ │ ├── InterfaceGenerator.cs
│ │ ├── InteropGenerator.cs
│ │ └── SerializerGenerator.cs
│ ├── Pack
│ │ ├── BindingGenerator
│ │ │ ├── BindingClassGenerator.cs
│ │ │ └── BindingGenerator.cs
│ │ ├── BootsharpPack.cs
│ │ ├── DeclarationGenerator
│ │ │ ├── DeclarationGenerator.cs
│ │ │ ├── MethodDeclarationGenerator.cs
│ │ │ └── TypeDeclarationGenerator.cs
│ │ ├── ModulePatcher
│ │ │ ├── InternalPatcher.cs
│ │ │ └── ModulePatcher.cs
│ │ └── ResourceGenerator.cs
│ └── Packer.csproj
├── Bootsharp.sln
├── Bootsharp
│ ├── Bootsharp.csproj
│ └── Build
│ │ ├── Bootsharp.props
│ │ ├── Bootsharp.targets
│ │ └── PackageTemplate.json
├── Directory.Build.props
└── nuget.config
└── js
├── package.json
├── scripts
├── build.sh
├── compile-test.sh
├── cover.sh
└── test.sh
├── src
├── bindings.g.ts
├── boot.ts
├── config.ts
├── decoder.ts
├── dotnet.g.d.ts
├── dotnet.native.g.d.ts
├── dotnet.runtime.g.d.ts
├── event.ts
├── exports.ts
├── imports.ts
├── index.ts
├── instances.ts
├── modules.ts
├── resources.g.ts
└── resources.ts
├── test
├── cs.ts
├── cs
│ ├── Test.Types
│ │ ├── Interfaces
│ │ │ ├── ExportedInstanced.cs
│ │ │ ├── ExportedStatic.cs
│ │ │ ├── IExportedInstanced.cs
│ │ │ ├── IExportedStatic.cs
│ │ │ ├── IImportedInstanced.cs
│ │ │ └── IImportedStatic.cs
│ │ ├── Test.Types.csproj
│ │ └── Vehicle
│ │ │ ├── IRegistryProvider.cs
│ │ │ ├── Registry.cs
│ │ │ ├── TrackType.cs
│ │ │ ├── Tracked.cs
│ │ │ ├── Vehicle.cs
│ │ │ └── Wheeled.cs
│ ├── Test.sln
│ ├── Test
│ │ ├── Event.cs
│ │ ├── Functions.cs
│ │ ├── IdxEnum.cs
│ │ ├── Invokable.cs
│ │ ├── Platform.cs
│ │ ├── Program.cs
│ │ └── Test.csproj
│ └── nuget.config
└── spec
│ ├── boot.spec.ts
│ ├── event.spec.ts
│ ├── export.spec.ts
│ ├── interop.spec.ts
│ └── platform.spec.ts
└── tsconfig.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: elringus
2 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: codeql
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | analyze:
11 | name: analyze
12 | runs-on: ubuntu-latest
13 | permissions:
14 | actions: read
15 | contents: read
16 | security-events: write
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | language: [ "csharp", "javascript" ]
21 | steps:
22 | - name: checkout
23 | uses: actions/checkout@v4
24 | - name: setup dotnet
25 | uses: actions/setup-dotnet@v4
26 | with:
27 | dotnet-version: 9
28 | - name: initialize codeql
29 | uses: github/codeql-action/init@v2
30 | with:
31 | languages: ${{ matrix.language }}
32 | - name: build
33 | run: |
34 | cd src/cs
35 | dotnet workload restore
36 | mkdir .nuget
37 | dotnet build Bootsharp.Generate -c Release
38 | dotnet build Bootsharp.Common -c Release
39 | dotnet pack Bootsharp.Common -o .nuget
40 | dotnet build -c Release
41 | dotnet pack Bootsharp -o .nuget
42 | cd ../js
43 | npm install
44 | bash scripts/build.sh
45 | - name: analyze
46 | uses: github/codeql-action/analyze@v2
47 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 |
3 | on:
4 | workflow_dispatch: { }
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | deploy:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | pages: write
14 | id-token: write
15 | environment:
16 | name: github-pages
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: fetch tags
20 | run: git fetch --prune --unshallow --tags
21 | - uses: actions/setup-node@v4
22 | - name: build
23 | run: |
24 | npm i --prefix src/js
25 | cd docs
26 | npm install
27 | npm run docs:api
28 | npm run docs:build
29 | - uses: actions/configure-pages@v5
30 | - uses: actions/upload-pages-artifact@v3
31 | with:
32 | path: docs/.vitepress/dist
33 | - uses: actions/deploy-pages@v4
34 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | editor:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: editorconfig
15 | run: |
16 | docker run --rm --volume=$PWD:/check mstruebing/editorconfig-checker ec --exclude .git
17 |
--------------------------------------------------------------------------------
/.github/workflows/package.yml:
--------------------------------------------------------------------------------
1 | name: package
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: checkout
11 | uses: actions/checkout@v4
12 | - name: setup dotnet
13 | uses: actions/setup-dotnet@v4
14 | with:
15 | dotnet-version: 9
16 | - name: package
17 | run: |
18 | cd src/js
19 | npm install
20 | bash scripts/build.sh
21 | cd ../cs
22 | dotnet workload restore
23 | mkdir .nuget
24 | dotnet build Bootsharp.Generate -c Release
25 | dotnet build Bootsharp.Common -c Release
26 | dotnet pack Bootsharp.Common -o .nuget -c Release
27 | dotnet pack Bootsharp.Inject -o .nuget -c Release
28 | dotnet build -c Release
29 | dotnet pack Bootsharp -o .nuget -c Release
30 | - name: publish to nuget
31 | run: |
32 | dotnet nuget push "src/cs/.nuget/Bootsharp*.nupkg" -k ${{ secrets.NUGET_KEY }} -s https://www.nuget.org --skip-duplicate
33 | - name: publish to github
34 | run: |
35 | dotnet nuget add source https://nuget.pkg.github.com/elringus/index.json -n github --username Elringus --password ${{ secrets.GH_KEY }} --store-password-in-clear-text
36 | dotnet nuget push "src/cs/.nuget/Bootsharp*.nupkg" -s github -k ${{ secrets.GH_KEY }} --skip-duplicate
37 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: stale
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '45 3 * * *'
7 |
8 | jobs:
9 | scan:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/stale@v9
13 | id: stale
14 | with:
15 | stale-issue-label: stale
16 | stale-pr-label: stale
17 | stale-issue-message: 'This issue is stale because it has been open 14 days with no activity. It will be automatically closed in 7 days.'
18 | stale-pr-message: 'This pull request is stale because it has been open 14 days with no activity. It will be automatically closed in 7 days.'
19 | days-before-stale: 14
20 | days-before-close: 7
21 | exempt-issue-labels: 'bug,enhancement'
22 | exempt-pr-labels: 'bug,enhancement'
23 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | cover:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: setup dotnet
15 | uses: actions/setup-dotnet@v4
16 | with:
17 | dotnet-version: 9
18 | - uses: actions/setup-node@v4
19 | with:
20 | node-version: 22
21 | - name: cover
22 | run: |
23 | cd src/js
24 | npm install
25 | bash scripts/build.sh
26 | cd ../cs
27 | dotnet workload restore
28 | mkdir .nuget
29 | dotnet build Bootsharp.Generate -c Release
30 | dotnet build Bootsharp.Common -c Release
31 | dotnet pack Bootsharp.Common -o .nuget
32 | dotnet build -c Release
33 | dotnet pack Bootsharp -o .nuget
34 | dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:ExcludeByAttribute=GeneratedCodeAttribute
35 | cd ../js/test/cs
36 | dotnet workload restore
37 | dotnet publish -p:BootsharpName=embedded -p:BootsharpEmbedBinaries=true
38 | dotnet publish -p:BootsharpName=sideload -p:BootsharpEmbedBinaries=false
39 | cd ../..
40 | npm run cover
41 | - name: upload coverage reports to Codecov
42 | uses: codecov/codecov-action@v4
43 | with:
44 | fail_ci_if_error: true
45 | env:
46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | .idea
3 | node_modules
4 | coverage
5 | cache
6 | dist
7 | bin
8 | obj
9 |
10 | *.suo
11 | *.user
12 | *.nupkg
13 | package-lock.json
14 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Artyom Sovetnikov (Elringus)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # Use C# in web apps with comfort
16 |
17 | Bootsharp streamlines the integration of .NET C# apps and libraries into web projects. It's ideal for building applications where the domain (backend) is authored in .NET C#, while the UI (frontend) is a standalone TypeScript or JavaScript project. Think of it as [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html) for C++ or [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) for Rust.
18 |
19 | 
20 |
21 | Facilitating high-level interoperation between C# and TypeScript, Bootsharp lets you build the UI layer within its natural ecosystem using industry-standard tooling and frameworks, such as [React](https://react.dev) and [Svelte](https://svelte.dev). The project can then be published to the web or bundled as a native desktop or mobile application with [Electron](https://electronjs.org) or [Tauri](https://tauri.app).
22 |
23 | ## Features
24 |
25 | ✨ High-level C# <-> TypeScript interop
26 |
27 | 📦 Embeds binaries to single-file ES module
28 |
29 | 🗺️ Works in browsers and JS runtimes (Node, Deno, Bun)
30 |
31 | ⚡ Generates bindings and types over C# interfaces
32 |
33 | 🏷️ Supports interop over object instances
34 |
35 | 🛠️ Allows customizing emitted bindings
36 |
37 | 🔥 Supports multi-threading, NativeAOT-LLVM, trimming
38 |
39 | ## 🎬 Get Started
40 |
41 | https://bootsharp.com/guide
42 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | cache
2 | api
3 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import DefaultTheme from "vitepress/theme-without-fonts";
2 | import "./style.css";
3 |
4 | // Have to import client assets manually due to vitepress
5 | // bug: https://github.com/vuejs/vitepress/issues/3314
6 | import "imgit/styles";
7 | import "imgit/client";
8 |
9 | // https://vitepress.dev/guide/extending-default-theme
10 | export default { extends: { Layout: DefaultTheme.Layout } };
11 |
--------------------------------------------------------------------------------
/docs/guide/emit-prefs.md:
--------------------------------------------------------------------------------
1 | # Emit Preferences
2 |
3 | Use `[JSPreferences]` assembly attribute to customize Bootsharp behaviour at build time when the interop code is emitted. It has several properties that takes array of `(pattern, replacement)` strings, which are feed to [Regex.Replace](https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.replace?view=net-6.0#system-text-regularexpressions-regex-replace(system-string-system-string-system-string)) when emitted associated code. Each consequent pair is tested in order; on first match the result replaces the default.
4 |
5 | ## Space
6 |
7 | By default, all the generated JavaScript binding objects and TypeScript declarations are grouped under corresponding C# namespaces; refer to [namespaces](/guide/namespaces) docs for more info.
8 |
9 | To customize emitted spaces, use `Space` parameter. For example, to make all bindings declared under "Foo.Bar" C# namespace have "Baz" namespace in JavaScript:
10 |
11 | ```cs
12 | [assembly: JSPreferences(
13 | Space = ["^Foo\.Bar\.(\S+)", "Baz.$1"]
14 | )]
15 | ```
16 |
17 | The patterns are matched against full type name of declaring C# type when generating JavaScript objects for interop methods and against namespace when generating TypeScript syntax for C# types. Matched type names have the following modifications:
18 |
19 | - interfaces have first character removed
20 | - generics have parameter spec removed
21 | - nested type names have `+` replaced with `.`
22 |
23 | ## Type
24 |
25 | Allows customizing generated TypeScript type syntax. The patterns are matched against full C# type names of interop method arguments, return values and object properties.
26 |
27 | ## Event
28 |
29 | Used to customize which C# methods should be transformed into JavaScript events, as well as generated event names. The patterns are matched against C# method names declared under `[JSImport]` interfaces. By default, methods starting with "Notify..." are matched and renamed to "On...".
30 |
31 | ## Function
32 |
33 | Customizes generated JavaScript function names. The patterns are matched against C# interop method names.
34 |
--------------------------------------------------------------------------------
/docs/guide/extensions/dependency-injection.md:
--------------------------------------------------------------------------------
1 | # Dependency Injection
2 |
3 | When using [interop interfaces](/guide/interop-interfaces), it's convenient to use a dependency injection mechanism to automatically route generated interop implementations for the services that needs them.
4 |
5 | Reference `Bootsharp.Inject` extension in the project configuration:
6 |
7 | ```xml
8 |
9 |
10 |
11 | net9.0
12 | browser-wasm
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | — and use `AddBootsharp` extension method to inject the generated import implementations; `RunBootsharp` will initialize generated export implementation by requiring the handlers, which should be added to the services collection before.
25 |
26 | ```csharp
27 | using Bootsharp;
28 | using Bootsharp.Inject;
29 | using Microsoft.Extensions.DependencyInjection;
30 |
31 | [assembly: JSExport(
32 | typeof(IExported),
33 | // other APIs to export to JavaScript
34 | )]
35 |
36 | [assembly: JSImport(
37 | typeof(IImported),
38 | // other APIs to import from JavaScript
39 | )]
40 |
41 | new ServiceCollection()
42 | // Inject generated implementation of IImported.
43 | .AddBootsharp()
44 | // Inject other services, which may require IImported.
45 | .AddSingleton()
46 | // Provide handler for the exported interface.
47 | .AddSingleton()
48 | // Build the collection.
49 | .BuildServiceProvider()
50 | // Initialize the exported implementations.
51 | .RunBootsharp();
52 | ```
53 |
54 | `IImported` can now be requested via .NET's DI, while `IExported` APIs are available in JavaScript:
55 |
56 | ```csharp
57 | public class SomeService (IImported imported) { }
58 | ```
59 |
60 | ```ts
61 | import { Exported } from "bootsharp";
62 | ```
63 |
64 | ::: tip EXAMPLE
65 | Find example on using the DI extension in the [React sample](https://github.com/elringus/bootsharp/blob/main/samples/react).
66 | :::
67 |
--------------------------------------------------------------------------------
/docs/guide/interop-instances.md:
--------------------------------------------------------------------------------
1 | # Interop Instances
2 |
3 | When an interface is supplied as argument or return type of an interop method, instead of serializing it as value, Bootsharp will instead generate an instance binding, eg:
4 |
5 | ```csharp
6 | public interface IExported { string GetFromCSharp (); }
7 | public interface IImported { string GetFromJavaScript (); }
8 |
9 | public class Exported : IExported
10 | {
11 | public string GetFromCSharp () => "cs";
12 | }
13 |
14 | public static partial class Factory
15 | {
16 | [JSInvokable] public static IExported GetExported () => new Exported();
17 | [JSFunction] public static partial IImported GetImported ();
18 | }
19 |
20 | var imported = Factory.GetImported();
21 | imported.GetFromJavaScript(); //returns "js"
22 | ```
23 |
24 | ```ts
25 | import { Factory, IImported } from "bootsharp";
26 |
27 | class Imported implements IImported {
28 | getFromJavaScript() { return "js"; }
29 | }
30 |
31 | Factory.getImported = () => new Imported();
32 |
33 | const exported = Factory.getExported();
34 | exported.getFromCSharp(); // returns "cs"
35 | ```
36 |
37 | Interop instances are subject to the following limitations:
38 | - Can't be args or return values of other interop instance method
39 | - Can't be args of events
40 | - Interfaces from "System" namespace are not qualified
41 |
--------------------------------------------------------------------------------
/docs/guide/interop-interfaces.md:
--------------------------------------------------------------------------------
1 | # Interop Interfaces
2 |
3 | Instead of manually authoring a binding for each method, let Bootsharp generate them automatically using the `[JSImport]` and `[JSExport]` assembly attributes.
4 |
5 | For example, say we have a JavaScript UI (frontend) that needs to be notified when data is mutated in the C# domain layer (backend), so it can render the updated state. Additionally, the frontend may have a setting (e.g., stored in the browser cache) to temporarily mute notifications, which the backend needs to retrieve. You can create the following interface in C# to describe the expected frontend APIs:
6 |
7 | ```csharp
8 | interface IFrontend
9 | {
10 | void NotifyDataChanged (Data data);
11 | bool IsMuted ();
12 | }
13 | ```
14 |
15 | Now, add the interface type to the JS import list:
16 |
17 | ```csharp
18 | [assembly: JSImport([
19 | typeof(IFrontend)
20 | ])]
21 | ```
22 |
23 | Bootsharp will automatically implement the interface in C#, wiring it to JavaScript, while also providing you with a TypeScript spec to implement on the frontend:
24 |
25 | ```ts
26 | export namespace Frontend {
27 | export const onDataChanged: Event<[Data]>;
28 | export let isMuted: () => boolean;
29 | }
30 | ```
31 |
32 | Now, say we want to provide an API for the frontend to request a mutation of the data:
33 |
34 | ```csharp
35 | interface IBackend
36 | {
37 | void AddData (Data data);
38 | }
39 | ```
40 |
41 | Export the interface to JavaScript:
42 |
43 | ```csharp
44 | [assembly: JSExport([
45 | typeof(IBackend)
46 | ])]
47 | ```
48 |
49 | This will generate the following implementation:
50 |
51 | ```csharp
52 | public class JSBackend
53 | {
54 | private static IBackend handler = null!;
55 |
56 | public JSBackend (IBackend handler)
57 | {
58 | JSBackend.handler = handler;
59 | }
60 |
61 | [JSInvokable]
62 | public static void AddData (Data data) => handler.AddData(data);
63 | }
64 | ```
65 |
66 | — which will produce the following spec to be consumed on the JavaScript side:
67 |
68 | ```ts
69 | export namespace Backend {
70 | export function addData(data: Data): void;
71 | }
72 | ```
73 |
74 | To make Bootsharp automatically inject and initialize the generated interop implementations, use the [dependency injection](/guide/extensions/dependency-injection) extension.
75 |
76 | ::: tip Example
77 | Find an example of using interop interfaces in the [React sample](https://github.com/elringus/bootsharp/tree/main/samples/react).
78 | :::
79 |
--------------------------------------------------------------------------------
/docs/guide/namespaces.md:
--------------------------------------------------------------------------------
1 | # Namespaces
2 |
3 | Bootsharp maps generated binding APIs based on the name of the associated C# types. The rules are a bit different for static interop methods, interop interfaces and types.
4 |
5 | ## Static Methods
6 |
7 | Full type name (including namespace) of the declaring type of the static interop method is mapped into JavaScript object name:
8 |
9 | ```csharp
10 | class Class { [JSInvokable] static void Method() {} }
11 | namespace Foo { class Class { [JSInvokable] static void Method() {} } }
12 | namespace Foo.Bar { class Class { [JSInvokable] static void Method() {} } }
13 | ```
14 |
15 | ```ts
16 | import { Class, Foo } from "bootsharp";
17 |
18 | Class.method();
19 | Foo.Class.method();
20 | Foo.Bar.Class.method();
21 | ```
22 |
23 | Methods inside nested classes are treated as if they were declared under namespace:
24 |
25 | ```csharp
26 | namespace Foo;
27 |
28 | public class Class
29 | {
30 | public class Nested { [JSInvokable] public static void Method() {} }
31 | }
32 | ```
33 |
34 | ```ts
35 | import { Foo } from "bootsharp";
36 |
37 | Foo.Class.Nested.method();
38 | ```
39 |
40 | ## Interop Interfaces
41 |
42 | When generating bindings for [interop interfaces](/guide/interop-interfaces), it's assumed the interface name has "I" prefix, so the associated implementation name will have first character removed. In case interface is declared under namespace, it'll be mirrored in JavaScript.
43 |
44 | ```csharp
45 | [JSExport([
46 | typeof(IExported),
47 | typeof(Foo.IExported),
48 | typeof(Foo.Bar.IExported),
49 | ])]
50 |
51 | interface IExported { void Method(); }
52 | namespace Foo { interface IExported { void Method(); } }
53 | namespace Foo.Bar { interface IExported { void Method(); } }
54 | ```
55 |
56 | ```ts
57 | import { Exported, Foo } from "bootsharp";
58 |
59 | Exported.method();
60 | Foo.Exported.method();
61 | Foo.Bar.Exported.method();
62 | ```
63 |
64 | ## Types
65 |
66 | Custom types referenced in API signatures (records, classes, interfaces, etc) are declared under their respective namespace when they have one, or under root otherwise.
67 |
68 | ```csharp
69 | public record Record;
70 | namespace Foo { public record Record; }
71 |
72 | partial class Class
73 | {
74 | [JSFunction]
75 | public static partial Record Method(Foo.Record r);
76 | }
77 | ```
78 |
79 | ```ts
80 | import { Class, Record, Foo } from "bootsharp";
81 |
82 | Class.method = methodImpl;
83 |
84 | function methodImpl(r: Record): Foo.Record {
85 |
86 | }
87 | ```
88 |
89 | ## Configuring Namespaces
90 |
91 | You can control how namespaces are generated via `Space` patterns of [emit preferences](/guide/emit-prefs).
92 |
--------------------------------------------------------------------------------
/docs/guide/sideloading.md:
--------------------------------------------------------------------------------
1 | # Sideloading Binaries
2 |
3 | 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.
4 |
5 | To disable the embedding, set `BootsharpEmbedBinaries` build property to false:
6 |
7 | ```xml
8 |
9 |
10 | false
11 |
12 | ```
13 |
14 | The `dotnet.wasm` and solution's assemblies will be emitted in the build output directory. You will then have to provide them when booting:
15 |
16 | ```ts
17 | const resources = {
18 | wasm: Uint8Array,
19 | assemblies: [{ name: "Foo.wasm", content: Uint8Array }],
20 | entryAssemblyName: "Foo.dll"
21 | };
22 | await dotnet.boot({ resources });
23 | ```
24 |
25 | — this way the binary files can be streamed directly from server to optimize traffic and initial load time.
26 |
27 | Alternatively, set `root` property of the boot options and Bootsharp will automatically fetch the resources form the specified URL:
28 |
29 | ```ts
30 | // Assuming the resources are stored in "bin" directory under website root.
31 | await backend.boot({ root: "/bin" });
32 | ```
33 |
34 | ::: tip EXAMPLE
35 | Find sideloading example in the [React sample](https://github.com/elringus/bootsharp/blob/main/samples/react).
36 | :::
37 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "scripts": {
4 | "docs:api": "sh scripts/api.sh",
5 | "docs:dev": "vitepress dev",
6 | "docs:build": "vitepress build",
7 | "docs:preview": "vitepress preview"
8 | },
9 | "devDependencies": {
10 | "typescript": "5.8.2",
11 | "@types/node": "22.15.17",
12 | "vitepress": "1.6.3",
13 | "typedoc-vitepress-theme": "1.1.2",
14 | "imgit": "0.2.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
23 |
--------------------------------------------------------------------------------
/docs/public/fonts/inter.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/fonts/inter.woff2
--------------------------------------------------------------------------------
/docs/public/fonts/jb.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/fonts/jb.woff2
--------------------------------------------------------------------------------
/docs/public/img/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/img/banner.png
--------------------------------------------------------------------------------
/docs/public/img/llvm-bench.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/img/llvm-bench.png
--------------------------------------------------------------------------------
/docs/public/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/img/logo.png
--------------------------------------------------------------------------------
/docs/public/img/og.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/img/og.jpg
--------------------------------------------------------------------------------
/docs/public/imgit/covers.json:
--------------------------------------------------------------------------------
1 | {"/img/banner.png":"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAGobWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAALGlsb2MAAAAARAAAAgABAAAAAQAAAdAAAAA6AAIAAAABAAACCgAAABcAAABCaWluZgAAAAAAAgAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAGmluZmUCAAAAAAIAAGF2MDFBbHBoYQAAAAAaaXJlZgAAAAAAAAAOYXV4bAACAAEAAQAAANdpcHJwAAAAsWlwY28AAAAUaXNwZQAAAAAAAAAiAAAAGQAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAQACAACAAAAAFGlzcGUAAAAAAAAAIgAAABkAAAAOcGl4aQAAAAABCAAAAAxhdjFDgQAcAAAAADhhdXhDAAAAAHVybjptcGVnOm1wZWdCOmNpY3A6c3lzdGVtczphdXhpbGlhcnk6YWxwaGEAAAAAHmlwbWEAAAAAAAAAAgABBAECgwQAAgQFBocIAAAAWW1kYXQKCDgVIcNICGgBMi4VwAggQQSBBADGj7ypuE7UHxPhV1G1GHE5FG93ilmzJJiYEiHAeEM2IxmfQp+aCgkYFSHDTAQICoAyChXAAAEgAAY04OY=","/img/llvm-bench.png":"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAGobWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAALGlsb2MAAAAARAAAAgABAAAAAQAAAdAAAACfAAIAAAABAAACbwAAADMAAABCaWluZgAAAAAAAgAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAGmluZmUCAAAAAAIAAGF2MDFBbHBoYQAAAAAaaXJlZgAAAAAAAAAOYXV4bAACAAEAAQAAANdpcHJwAAAAsWlwY28AAAAUaXNwZQAAAAAAAAAiAAAAGQAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAgACAACAAAAAFGlzcGUAAAAAAAAAIgAAABkAAAAOcGl4aQAAAAABCAAAAAxhdjFDgQAcAAAAADhhdXhDAAAAAHVybjptcGVnOm1wZWdCOmNpY3A6c3lzdGVtczphdXhpbGlhcnk6YWxwaGEAAAAAHmlwbWEAAAAAAAAAAgABBAECgwQAAgQFBocIAAAA2m1kYXQKCDgVIcNICGgBMpIBFcAAAEhRAMaOA+FtV3ADXTXH0icNRhuyF0mNhCGSv5LA9y82YVht9ZiZYzEnsTPARuD6eJnOlxsf1vtBaPTZ7iyPjipkbYkXKTt4KWlPVHe14wdrNp7lZqbmAuE7riBoyAIEoKG92J1nbTN6tMafQJVq/rliKLmG21pc1MA8unt5w5MoSEj1Yjpa3nliSOjCvoAKBhgVIcNKgDIpFcAIISBA168tFtIk7x1uhDQrbZvAyBS3lwml3DDvyCh1UUW+IIQkcPA="}
--------------------------------------------------------------------------------
/docs/public/imgit/encoded/img-banner.png@cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/imgit/encoded/img-banner.png@cover.avif
--------------------------------------------------------------------------------
/docs/public/imgit/encoded/img-banner.png@main.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/imgit/encoded/img-banner.png@main.avif
--------------------------------------------------------------------------------
/docs/public/imgit/encoded/img-llvm-bench.png@cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/imgit/encoded/img-llvm-bench.png@cover.avif
--------------------------------------------------------------------------------
/docs/public/imgit/encoded/img-llvm-bench.png@dense.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/imgit/encoded/img-llvm-bench.png@dense.avif
--------------------------------------------------------------------------------
/docs/public/imgit/encoded/img-llvm-bench.png@main.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/docs/public/imgit/encoded/img-llvm-bench.png@main.avif
--------------------------------------------------------------------------------
/docs/public/imgit/probes.json:
--------------------------------------------------------------------------------
1 | {"/img/banner.png":{"width":830,"height":610,"alpha":true,"type":"image/png"},"/img/llvm-bench.png":{"width":1820,"height":1320,"alpha":true,"type":"image/png"}}
--------------------------------------------------------------------------------
/docs/public/imgit/sizes.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/docs/public/imgit/specs.json:
--------------------------------------------------------------------------------
1 | {"/img/banner.png@main":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5","scale":0.8289156626506025},"/img/llvm-bench.png@main":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5","scale":0.378021978021978},"/img/banner.png@cover":{"ext":"avif","select":0,"scale":0.041445783132530126,"blur":0.4,"codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"},"/img/llvm-bench.png@dense":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"},"/img/llvm-bench.png@cover":{"ext":"avif","select":0,"scale":0.018901098901098902,"blur":0.4,"codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"}}
--------------------------------------------------------------------------------
/docs/scripts/api.sh:
--------------------------------------------------------------------------------
1 | # https://typedoc-plugin-markdown.org/themes/vitepress/quick-start
2 |
3 | echo '{
4 | "entryPoints": [
5 | "../src/js/src/index.ts"
6 | ],
7 | "tsconfig": "../src/js/tsconfig.json",
8 | "out": "api",
9 | "name": "Bootsharp",
10 | "readme": "none",
11 | "githubPages": false,
12 | "useCodeBlocks": true,
13 | "hideGenerator": true,
14 | "hideBreadcrumbs": true,
15 | "textContentMappings": {
16 | "title.indexPage": "API Reference",
17 | "title.memberPage": "{name}",
18 | },
19 | "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"]
20 | }' > typedoc.json
21 |
22 | typedoc
23 | sed -i -z "s/API Reference/API Reference\nAuto-generated with [typedoc-plugin-markdown](https:\/\/typedoc-plugin-markdown.org)./" api/index.md
24 | rm typedoc.json
25 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "target": "esnext",
5 | "moduleResolution": "bundler"
6 | },
7 | "include": [".vitepress/**/*"]
8 | }
9 |
--------------------------------------------------------------------------------
/samples/bench/bootsharp/Boot.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0-browser
5 | browser-wasm
6 |
7 | false
8 | speed
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | true
21 | none
22 | $(EmccFlags) -O3
23 | false
24 |
25 | https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/samples/bench/bootsharp/Program.cs:
--------------------------------------------------------------------------------
1 | using Bootsharp;
2 | using Bootsharp.Inject;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | [assembly: JSImport(typeof(IImport))]
6 | [assembly: JSExport(typeof(IExport))]
7 |
8 | new ServiceCollection()
9 | .AddBootsharp()
10 | .AddSingleton()
11 | .BuildServiceProvider()
12 | .RunBootsharp();
13 |
14 | public struct Data
15 | {
16 | public string Info { get; set; }
17 | public bool Ok { get; set; }
18 | public int Revision { get; set; }
19 | public string[] Messages { get; set; }
20 | }
21 |
22 | public interface IImport
23 | {
24 | int GetNumber ();
25 | Data GetStruct ();
26 | }
27 |
28 | public interface IExport
29 | {
30 | int EchoNumber ();
31 | Data EchoStruct ();
32 | int Fi (int n);
33 | }
34 |
35 | public class Export (IImport import) : IExport
36 | {
37 | public int EchoNumber () => import.GetNumber();
38 | public Data EchoStruct () => import.GetStruct();
39 | public int Fi (int n) => F(n);
40 | // Due to heavy recursion, a significant degradation accumulates due to constant
41 | // dereferencing of the instance on each iteration, hence using the static version.
42 | private static int F (int n) => n <= 1 ? n : F(n - 1) + F(n - 2);
43 | }
44 |
--------------------------------------------------------------------------------
/samples/bench/bootsharp/init.mjs:
--------------------------------------------------------------------------------
1 | import bootsharp, { Export, Import } from "./bin/bootsharp/index.mjs";
2 | import { getNumber, getStruct } from "../fixtures.mjs";
3 | import fs from "fs/promises";
4 |
5 | /** @returns {Promise} */
6 | export async function init() {
7 | Import.getNumber = getNumber;
8 | Import.getStruct = getStruct;
9 |
10 | const content = await fs.readFile("./bootsharp/bin/bootsharp/bin/dotnet.native.wasm");
11 | await bootsharp.boot({
12 | root: "./bin",
13 | resources: {
14 | wasm: { name: "dotnet.native.wasm", content },
15 | assemblies: [],
16 | entryAssemblyName: "Boot.dll"
17 | }
18 | });
19 |
20 | return { ...Export };
21 | }
22 |
--------------------------------------------------------------------------------
/samples/bench/bootsharp/readme.md:
--------------------------------------------------------------------------------
1 | 1. Install .NET https://dotnet.microsoft.com/en-us/download
2 | 2. Run `dotnet publish`
3 |
--------------------------------------------------------------------------------
/samples/bench/dotnet-llvm/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | public struct Data
6 | {
7 | public string Info { get; set; }
8 | public bool Ok { get; set; }
9 | public int Revision { get; set; }
10 | public string[] Messages { get; set; }
11 | }
12 |
13 | [JsonSerializable(typeof(Data))]
14 | [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
15 | internal partial class SourceGenerationContext : JsonSerializerContext;
16 |
17 | public static unsafe class Program
18 | {
19 | public static void Main () { }
20 |
21 | [UnmanagedCallersOnly(EntryPoint = "NativeLibrary_Free")]
22 | public static void Free (void* p) => NativeMemory.Free(p);
23 |
24 | [UnmanagedCallersOnly(EntryPoint = "echoNumber")]
25 | public static int EchoNumber () => GetNumber();
26 |
27 | [UnmanagedCallersOnly(EntryPoint = "echoStruct")]
28 | public static char* EchoStruct ()
29 | {
30 | var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(GetStruct());
31 | var data = JsonSerializer.Deserialize(span, SourceGenerationContext.Default.Data);
32 | var json = JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data);
33 | fixed (char* ptr = json) return ptr; // has to be pinned and freed after use in real use cases
34 | }
35 |
36 | [UnmanagedCallersOnly(EntryPoint = "fi")]
37 | public static int FiExport (int n) => Fi(n);
38 | private static int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2);
39 |
40 | [DllImport("x", EntryPoint = "getNumber")]
41 | private static extern int GetNumber ();
42 |
43 | [DllImport("x", EntryPoint = "getStruct")]
44 | private static extern char* GetStruct ();
45 | }
46 |
47 | // NOTE: 95% of degradation compared to Rust is in the JSON de-/serialization.
48 | // GenerationMode = JsonSourceGenerationMode.Serialization is only implemented for serialization
49 | // and throws when used for de-serialization: https://github.com/dotnet/runtime/issues/55043.
50 |
--------------------------------------------------------------------------------
/samples/bench/dotnet-llvm/imports.js:
--------------------------------------------------------------------------------
1 | // TODO: Figure how to get fixtures from "../fixtures.mjs"
2 |
3 | mergeInto(LibraryManager.library, {
4 | getNumber: () => 42,
5 | getStruct: () => {
6 | const data = {
7 | info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
8 | ok: true,
9 | revision: -112,
10 | messages: ["foo", "bar", "baz", "nya", "far"]
11 | };
12 | const json = JSON.stringify(data);
13 | const size = lengthBytesUTF16(json) + 1;
14 | const ptr = _malloc(size);
15 | stringToUTF16(json, ptr, size);
16 | return ptr; // has to be freed after use in real use cases
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/samples/bench/dotnet-llvm/init.mjs:
--------------------------------------------------------------------------------
1 | import { dotnet } from "./bin/Release/net9.0-browser/browser-wasm/publish/dotnet.js";
2 |
3 | /** @returns {Promise} */
4 | export async function init() {
5 | const runtime = await dotnet.withDiagnosticTracing(false).create();
6 | await runtime.runMain("DotNetLLVM", []);
7 |
8 | return {
9 | echoNumber: runtime.Module._echoNumber,
10 | echoStruct: () => JSON.parse(runtime.Module.UTF16ToString(runtime.Module._echoStruct())),
11 | fi: runtime.Module._fi
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/samples/bench/dotnet-llvm/readme.md:
--------------------------------------------------------------------------------
1 | 1. Install .NET https://dotnet.microsoft.com/en-us/download
2 | 2. Install Binaryen https://github.com/WebAssembly/binaryen
3 | 3. Run `dotnet publish`
4 | 4. Run `wasm-opt bin/Release/net9.0-browser/browser-wasm/publish/dotnet.native.wasm -O3 -o bin/Release/net9.0-browser/browser-wasm/publish/dotnet.native.wasm --all-features --strip-dwarf --strip-debug --vacuum`
5 |
6 | https://github.com/dotnet/runtime/issues/113979#issuecomment-2759220563
7 |
--------------------------------------------------------------------------------
/samples/bench/dotnet/DotNet.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | browser-wasm
5 | true
6 | true
7 | Speed
8 | true
9 |
10 |
11 |
12 |
13 | <_Parameter1>browser
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/bench/dotnet/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices.JavaScript;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | public struct Data
6 | {
7 | public string Info { get; set; }
8 | public bool Ok { get; set; }
9 | public int Revision { get; set; }
10 | public string[] Messages { get; set; }
11 | }
12 |
13 | [JsonSerializable(typeof(Data))]
14 | [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
15 | internal partial class SourceGenerationContext : JsonSerializerContext;
16 |
17 | public static partial class Program
18 | {
19 | public static void Main () { }
20 |
21 | [JSExport]
22 | public static int EchoNumber () => GetNumber();
23 |
24 | [JSExport]
25 | public static string EchoStruct ()
26 | {
27 | var json = GetStruct();
28 | var data = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Data);
29 | return JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data);
30 | }
31 |
32 | [JSExport]
33 | public static int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2);
34 |
35 | [JSImport("getNumber", "x")]
36 | private static partial int GetNumber ();
37 |
38 | [JSImport("getStruct", "x")]
39 | private static partial string GetStruct ();
40 | }
41 |
--------------------------------------------------------------------------------
/samples/bench/dotnet/init.mjs:
--------------------------------------------------------------------------------
1 | import { dotnet } from "./bin/Release/net9.0/browser-wasm/AppBundle/_framework/dotnet.js";
2 | import { getNumber, getStruct } from "../fixtures.mjs";
3 |
4 | /** @returns {Promise} */
5 | export async function init() {
6 | const runtime = await dotnet.withDiagnosticTracing(false).create();
7 | const asm = runtime.getConfig().mainAssemblyName;
8 |
9 | runtime.setModuleImports("x", {
10 | getNumber,
11 | getStruct: () => JSON.stringify(getStruct())
12 | });
13 |
14 | await runtime.runMain(asm, []);
15 |
16 | const exports = await runtime.getAssemblyExports(asm);
17 | return {
18 | echoNumber: exports.Program.EchoNumber,
19 | echoStruct: () => JSON.parse(exports.Program.EchoStruct()),
20 | fi: exports.Program.Fi
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/samples/bench/dotnet/readme.md:
--------------------------------------------------------------------------------
1 | 1. Install .NET https://dotnet.microsoft.com/en-us/download
2 | 2. Install Binaryen https://github.com/WebAssembly/binaryen
3 | 3. Run `dotnet publish`
4 | 4. Run `wasm-opt bin/Release/net9.0/browser-wasm/publish/dotnet.native.wasm -O3 -o bin/Release/net9.0/browser-wasm/publish/dotnet.native.wasm --all-features --strip-dwarf --strip-debug --vacuum`
5 |
--------------------------------------------------------------------------------
/samples/bench/fixtures.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} Data
3 | * @property {string} info
4 | * @property {boolean} ok
5 | * @property {number} revision
6 | * @property {string[]} messages
7 | */
8 |
9 | /** @returns {number} */
10 | export const getNumber = () => 42;
11 |
12 | /** @returns {Data} */
13 | export const getStruct = () => ({
14 | info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
15 | ok: true,
16 | revision: -112,
17 | messages: ["foo", "bar", "baz", "nya", "far"]
18 | });
19 |
--------------------------------------------------------------------------------
/samples/bench/go/.gitignore:
--------------------------------------------------------------------------------
1 | *.wasm
2 | wasm_exec.js
3 |
--------------------------------------------------------------------------------
/samples/bench/go/init.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import "./wasm_exec.js";
3 | import { getNumber, getStruct } from "../fixtures.mjs";
4 |
5 | /** @returns {Promise} */
6 | export async function init() {
7 | global.getNumber = getNumber;
8 | global.getStruct = () => JSON.stringify(getStruct());
9 |
10 | const bin = await WebAssembly.compile(fs.readFileSync("./go/main.wasm"));
11 | const go = new Go();
12 | const wasm = await WebAssembly.instantiate(bin, go.importObject);
13 | go.run(wasm);
14 |
15 | return {
16 | echoNumber: global.echoNumber,
17 | echoStruct: () => JSON.parse(global.echoStruct()),
18 | fi: global.fi
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/samples/bench/go/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "syscall/js"
6 | )
7 |
8 | type Data struct {
9 | Info string `json:"info"`
10 | Ok bool `json:"ok"`
11 | Revision int `json:"revision"`
12 | Messages []string `json:"messages"`
13 | }
14 |
15 | func main() {
16 | js.Global().Set("echoNumber", js.FuncOf(echoNumber))
17 | js.Global().Set("echoStruct", js.FuncOf(echoStruct))
18 | js.Global().Set("fi", js.FuncOf(fi))
19 | <-make(chan struct{})
20 | }
21 |
22 | func echoNumber(_ js.Value, _ []js.Value) any {
23 | return js.Global().Call("getNumber").Int()
24 | }
25 |
26 | func echoStruct(_ js.Value, _ []js.Value) any {
27 | jsonStr := js.Global().Call("getStruct").String()
28 | var data Data
29 | if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
30 | return "Error: " + err.Error()
31 | }
32 | resultJson, _ := json.Marshal(data)
33 | return string(resultJson)
34 | }
35 |
36 | func fi(_ js.Value, args []js.Value) any {
37 | n := args[0].Int()
38 | return fibonacci(n)
39 | }
40 |
41 | func fibonacci(n int) int {
42 | if n <= 1 {
43 | return n
44 | }
45 | return fibonacci(n-1) + fibonacci(n-2)
46 | }
47 |
--------------------------------------------------------------------------------
/samples/bench/go/readme.md:
--------------------------------------------------------------------------------
1 | 1. Install Go https://go.dev/dl
2 | 2. Install Binaryen https://github.com/WebAssembly/binaryen
3 | 3. Copy `{GO_INSTALL_DIR}/lib/wasm/wasm_exec.js` to this folder
4 | 4. Run `& { $env:GOOS="js"; $env:GOARCH="wasm"; go build -o main.wasm main.go }`
5 | 5. Run `wasm-opt main.wasm -O3 -o main.wasm --all-features --strip-dwarf --strip-debug --vacuum`
6 |
--------------------------------------------------------------------------------
/samples/bench/readme.md:
--------------------------------------------------------------------------------
1 | ## Setup
2 |
3 | 1. Build each sub-dir (readme inside)
4 | 2. Run `node --expose-gc bench.mjs` to bench all
5 | 3. Add `rust|zig|llvm|net|boot|go` to bench specific
6 |
7 | ## Benches
8 |
9 | - `Fibonacci` — compute with heavy recursion
10 | - `Echo Number` — interop with raw numbers
11 | - `Echo Struct` — interop with JSON-serialized structs
12 |
13 | All results are relative to the Rust baseline (lower is better).
14 |
15 | ## 2024 (.NET 9)
16 |
17 | | | Rust | Zig | .NET LLVM | Bootsharp | .NET AOT | Go |
18 | |-------------|-------|-------|-----------|-----------|----------|---------|
19 | | Fibonacci | `1.0` | `0.9` | `1.0` | `1.0` | `1.6` | `3.7` |
20 | | Echo Number | `1.0` | `0.8` | `1.6` | `14.0` | `23.5` | `718.7` |
21 | | Echo Struct | `1.0` | `1.0` | `2.0` | `2.5` | `5.9` | `15.2` |
22 |
--------------------------------------------------------------------------------
/samples/bench/rust/.gitignore:
--------------------------------------------------------------------------------
1 | pkg
2 | target
3 | Cargo.lock
4 |
--------------------------------------------------------------------------------
/samples/bench/rust/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rust-wasm"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | wasm-bindgen = "0.2"
11 | serde = { version = "1.0", features = ["derive"] }
12 | serde_json = "1.0"
13 |
14 | [profile.release]
15 | codegen-units = 1
16 | lto = true
17 | opt-level = 3
18 | panic = "abort"
19 |
--------------------------------------------------------------------------------
/samples/bench/rust/init.mjs:
--------------------------------------------------------------------------------
1 | import { echoNumber, echoStruct, fi } from './pkg/rust_wasm.js';
2 | import { getNumber, getStruct } from "../fixtures.mjs";
3 |
4 | /** @returns {Promise} */
5 | export async function init() {
6 | global.getNumber = getNumber;
7 | global.getStruct = () => JSON.stringify(getStruct());
8 | return {
9 | echoNumber,
10 | echoStruct: () => JSON.parse(echoStruct()),
11 | fi
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/samples/bench/rust/readme.md:
--------------------------------------------------------------------------------
1 | 1. Install Rust https://rustup.rs
2 | 2. Install wasm-pack: https://rustwasm.github.io/wasm-pack
3 | 3. Run `wasm-pack build --target nodejs`
4 |
--------------------------------------------------------------------------------
/samples/bench/rust/src/lib.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use wasm_bindgen::prelude::*;
3 |
4 | #[derive(Serialize, Deserialize)]
5 | pub struct Data {
6 | pub info: String,
7 | pub ok: bool,
8 | pub revision: i32,
9 | pub messages: Vec,
10 | }
11 |
12 | #[wasm_bindgen]
13 | extern "C" {
14 | #[wasm_bindgen(js_name = getNumber)]
15 | fn get_number() -> i32;
16 | #[wasm_bindgen(js_name = getStruct)]
17 | fn get_struct() -> String;
18 | }
19 |
20 | #[wasm_bindgen(js_name = echoNumber)]
21 | pub fn echo_number() -> i32 {
22 | get_number()
23 | }
24 |
25 | #[wasm_bindgen(js_name = echoStruct)]
26 | pub fn echo_struct() -> String {
27 | let json = get_struct();
28 | let data: Data = serde_json::from_str(&json).unwrap();
29 | serde_json::to_string(&data).unwrap()
30 | }
31 |
32 | #[wasm_bindgen]
33 | pub fn fi(n: i32) -> i32 {
34 | if n <= 1 {
35 | return n;
36 | }
37 | fi(n - 1) + fi(n - 2)
38 | }
39 |
--------------------------------------------------------------------------------
/samples/bench/zig/.gitignore:
--------------------------------------------------------------------------------
1 | .zig-cache
2 | zit-out
3 |
--------------------------------------------------------------------------------
/samples/bench/zig/build.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub fn build(b: *std.Build) void {
4 | const lib = b.addExecutable(.{
5 | .name = "zig",
6 | .root_source_file = b.path("main.zig"),
7 | .target = b.resolveTargetQuery(.{
8 | .cpu_arch = .wasm32,
9 | .os_tag = .freestanding,
10 | .cpu_features_add = std.Target.wasm.featureSet(&.{
11 | .simd128,
12 | .relaxed_simd,
13 | .tail_call,
14 | }),
15 | }),
16 | .use_llvm = true,
17 | .use_lld = true,
18 | .optimize = b.standardOptimizeOption(.{}),
19 | });
20 | lib.entry = .disabled;
21 | lib.rdynamic = true;
22 | lib.want_lto = true;
23 | b.installArtifact(lib);
24 | }
25 |
--------------------------------------------------------------------------------
/samples/bench/zig/init.mjs:
--------------------------------------------------------------------------------
1 | import { getNumber, getStruct } from "../fixtures.mjs";
2 | import fs from "fs/promises";
3 |
4 | /** @returns {Promise} */
5 | export async function init() {
6 | const source = await fs.readFile("./zig/zig-out/bin/zig.wasm");
7 | const { instance: { exports } } = await WebAssembly.instantiate(source, {
8 | x: {
9 | getNumber,
10 | getStruct: () => encodeString(JSON.stringify(getStruct())),
11 | }
12 | });
13 | memory = exports.memory, cached = new Uint8Array(memory.buffer);
14 |
15 | return {
16 | echoNumber: exports.echoNumber,
17 | echoStruct: () => JSON.parse(decodeString(exports.echoStruct())),
18 | fi: exports.fi
19 | };
20 | }
21 |
22 | let memory, cached;
23 | const encoder = new TextEncoder("utf-8");
24 | const decoder = new TextDecoder("utf-8");
25 | const mask = BigInt("0xFFFFFFFF");
26 |
27 | function encodeString(str) {
28 | const memory = getMemoryCached();
29 | const { written } = encoder.encodeInto(str, memory);
30 | return BigInt(written) << BigInt(32) | BigInt(0);
31 | }
32 |
33 | function decodeString(ptrAndLen) {
34 | const memory = getMemoryCached();
35 | const ptr = Number(ptrAndLen & mask);
36 | const len = Number(ptrAndLen >> BigInt(32));
37 | const bytes = memory.subarray(ptr, ptr + len);
38 | return decoder.decode(bytes);
39 | }
40 |
41 | function getMemoryCached() {
42 | if (cached.buffer === memory.buffer) return cached;
43 | return cached = new Uint8Array(memory.buffer);
44 | }
45 |
--------------------------------------------------------------------------------
/samples/bench/zig/main.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | var arena = std.heap.ArenaAllocator.init(std.heap.wasm_allocator);
4 | const ally = arena.allocator();
5 |
6 | const opt = .{
7 | .parse = std.json.ParseOptions{
8 | .ignore_unknown_fields = true,
9 | },
10 | .stringify = std.json.StringifyOptions{
11 | .whitespace = .minified,
12 | },
13 | };
14 |
15 | pub const Data = struct {
16 | info: []const u8,
17 | ok: bool,
18 | revision: i32,
19 | messages: []const []const u8,
20 | };
21 |
22 | extern "x" fn getNumber() i32;
23 | extern "x" fn getStruct() u64;
24 |
25 | export fn echoNumber() i32 {
26 | return getNumber();
27 | }
28 |
29 | export fn echoStruct() u64 {
30 | _ = arena.reset(.retain_capacity);
31 | const input = decodeString(getStruct());
32 | const json = std.json.parseFromSlice(Data, ally, input, opt.parse) catch unreachable;
33 | var output = std.ArrayList(u8).init(ally);
34 | std.json.stringify(json.value, opt.stringify, output.writer()) catch unreachable;
35 | return encodeString(output.items);
36 | }
37 |
38 | export fn fi(n: i32) i32 {
39 | if (n <= 1) return n;
40 | return fi(n - 1) + fi(n - 2);
41 | }
42 |
43 | fn decodeString(ptr_and_len: u64) []const u8 {
44 | const ptr = @as(u32, @truncate(ptr_and_len));
45 | const len = @as(u32, @truncate(ptr_and_len >> 32));
46 | return @as([*]const u8, @ptrFromInt(ptr))[0..len];
47 | }
48 |
49 | fn encodeString(str: []const u8) u64 {
50 | return (@as(u64, str.len) << 32) | @intFromPtr(str.ptr);
51 | }
52 |
--------------------------------------------------------------------------------
/samples/bench/zig/readme.md:
--------------------------------------------------------------------------------
1 | 1. Install Zig https://ziglang.org/download/
2 | 2. Install Binaryen https://github.com/WebAssembly/binaryen
3 | 3. Run `zig build -Doptimize=ReleaseFast`
4 | 4. Run `wasm-opt zig-out/bin/zig.wasm -O3 -o zig-out/bin/zig.wasm --all-features --strip-dwarf --strip-debug --vacuum`
5 |
--------------------------------------------------------------------------------
/samples/minimal/README.md:
--------------------------------------------------------------------------------
1 | Minimal example on using Bootsharp in web browsers and popular JavaScript runtimes.
2 |
3 | - Run `dotnet publish cs`;
4 | - Run `node main.mjs` to test in [Node](https://nodejs.org);
5 | - Run `deno run main.mjs` to test in [Deno](https://deno.com);
6 | - Run `bun main.mjs` to test in [Bun](https://bun.sh);
7 | - Run an HTML server (eg, `npx serve`) to test in browser.
8 |
--------------------------------------------------------------------------------
/samples/minimal/cs/Minimal.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | browser-wasm
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/minimal/cs/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Bootsharp;
3 |
4 | public static partial class Program
5 | {
6 | public static void Main ()
7 | {
8 | OnMainInvoked($"Hello {GetFrontendName()}, .NET here!");
9 | }
10 |
11 | [JSEvent] // Used in JS as Program.onMainInvoked.subscribe(...)
12 | public static partial void OnMainInvoked (string message);
13 |
14 | [JSFunction] // Assigned in JS as Program.getFrontendName = ...
15 | public static partial string GetFrontendName ();
16 |
17 | [JSInvokable] // Invoked from JS as Program.GetBackendName()
18 | public static string GetBackendName () => $".NET {Environment.Version}";
19 | }
20 |
--------------------------------------------------------------------------------
/samples/minimal/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Loading...
4 |
5 |
23 |
--------------------------------------------------------------------------------
/samples/minimal/main.mjs:
--------------------------------------------------------------------------------
1 | // Named exports are auto-generated on C# build.
2 | import bootsharp, { Program } from "./cs/bin/bootsharp/index.mjs";
3 |
4 | // Binding 'Program.GetFrontendName' endpoint invoked in C#.
5 | Program.getFrontendName = () =>
6 | typeof Bun === "object" ? `Bun ${Bun.version}` :
7 | typeof Deno === "object" ? `Deno ${Deno.version.deno}` :
8 | typeof process === "object" ? `Node ${process.version}` :
9 | "Unknown JavaScript Runtime";
10 |
11 | // Subscribing to 'Program.OnMainInvoked' C# event.
12 | Program.onMainInvoked.subscribe(console.log);
13 |
14 | // Initializing dotnet runtime and invoking entry point.
15 | await bootsharp.boot();
16 |
17 | // Invoking 'Program.GetBackendName' C# method.
18 | console.log(`Hello ${Program.getBackendName()}!`);
19 |
--------------------------------------------------------------------------------
/samples/react/README.md:
--------------------------------------------------------------------------------
1 | Sample web application built with C# backend and [React](https://react.dev) frontend bundled with [Vite](https://vitejs.dev). Features generating JavaScript bindings for a standalone C# project and injecting them via [Microsoft.Extensions.DependencyInjection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection), multithreading, AOT-compiling, customizing various Bootsharp build options, side-loading binaries, mocking C# APIs in frontend unit tests, using events and type declarations.
2 |
3 | How to test:
4 | - Run `npm run backend` to compile C# backend;
5 | - Run `npm install` to install NPM dependencies;
6 | - Run `npm run test` to run frontend unit tests;
7 | - Run `npm run cover` to gather code coverage;
8 | - Run `npm run dev` to run local dev server with hot reload;
9 | - Run `npm run build` to build the app for production;
10 | - Run `npm run preview` to run local server for the built app.
11 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.Prime/Backend.Prime.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.Prime/IPrimeUI.cs:
--------------------------------------------------------------------------------
1 | namespace Backend.Prime;
2 |
3 | // Contract of the prime computer user interface.
4 | // The implementation goes to the frontend,
5 | // so that backend is not coupled with the details.
6 |
7 | public interface IPrimeUI
8 | {
9 | Options GetOptions ();
10 | // Imported methods starting with "Notify" will automatically
11 | // be converted to JavaScript events and renamed to "On...".
12 | // This can be configured with "JSImport.EventPattern" and
13 | // "JSImport.EventReplacement" attribute parameters.
14 | void NotifyComputing (bool computing);
15 | void NotifyComplete (long time);
16 | }
17 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.Prime/Options.cs:
--------------------------------------------------------------------------------
1 | namespace Backend.Prime;
2 |
3 | public record Options (int Complexity, bool Multithreading);
4 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.Prime/Prime.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace Backend.Prime;
4 |
5 | // Implementation of the computer service that compute prime numbers.
6 | // Injected in the application entry point assembly (Backend.WASM).
7 |
8 | public class Prime (IPrimeUI ui) : IComputer
9 | {
10 | private static readonly SemaphoreSlim semaphore = new(0);
11 | private readonly Stopwatch watch = new();
12 | private CancellationTokenSource? cts;
13 |
14 | public void StartComputing ()
15 | {
16 | cts?.Cancel();
17 | cts = new CancellationTokenSource();
18 | cts.Token.Register(() => ui.NotifyComputing(false));
19 | var options = ui.GetOptions();
20 | if (!options.Multithreading) ComputeLoop(options.Complexity, cts.Token);
21 | else new Thread(() => ComputeLoop(options.Complexity, cts.Token)).Start();
22 | ObserveLoop(cts.Token);
23 | ui.NotifyComputing(true);
24 | }
25 |
26 | public void StopComputing () => cts?.Cancel();
27 |
28 | public bool IsComputing () => !cts?.IsCancellationRequested ?? false;
29 |
30 | private async void ObserveLoop (CancellationToken token)
31 | {
32 | while (!token.IsCancellationRequested)
33 | {
34 | watch.Restart();
35 | try { await semaphore.WaitAsync(token); }
36 | catch (OperationCanceledException) { }
37 | finally
38 | {
39 | watch.Stop();
40 | ui.NotifyComplete(watch.ElapsedMilliseconds);
41 | }
42 | await Task.Delay(1);
43 | }
44 | }
45 |
46 | private static async void ComputeLoop (int complexity, CancellationToken token)
47 | {
48 | while (!token.IsCancellationRequested)
49 | {
50 | ComputePrime(complexity);
51 | semaphore.Release();
52 | await Task.Delay(10);
53 | }
54 | }
55 |
56 | private static void ComputePrime (int n)
57 | {
58 | var count = 0;
59 | var a = (long)2;
60 | while (count < n)
61 | {
62 | var b = (long)2;
63 | var prime = 1;
64 | while (b * b <= a)
65 | {
66 | if (a % b == 0)
67 | {
68 | prime = 0;
69 | break;
70 | }
71 | b++;
72 | }
73 | if (prime > 0) count++;
74 | a++;
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.WASM/Backend.WASM.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | browser-wasm
6 | enable
7 |
8 | backend
9 |
10 | $(SolutionDir)
11 |
12 | false
13 |
14 | $(SolutionDir)../public/bin
15 |
16 | true
17 |
18 | true
19 |
20 | true
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.WASM/Program.cs:
--------------------------------------------------------------------------------
1 | using Bootsharp;
2 | using Bootsharp.Inject;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | // Application entry point for browser-wasm build target.
6 | // Notice, how neither domain, nor other C# backend assemblies
7 | // are coupled with the JavaScript interop specifics
8 | // and can be shared with other build targets (console, MAUI, etc).
9 |
10 | // Generate C# -> JavaScript interop handlers for specified contracts.
11 | [assembly: JSExport(typeof(Backend.IComputer))]
12 | // Generate JavaScript -> C# interop handlers for specified contracts.
13 | [assembly: JSImport(typeof(Backend.Prime.IPrimeUI))]
14 | // Group all generated JavaScript APIs under "Computer" namespace.
15 | [assembly: JSPreferences(Space = [".+", "Computer"])]
16 |
17 | // Perform dependency injection.
18 | new ServiceCollection()
19 | .AddSingleton() // use prime computer
20 | .AddBootsharp() // inject generated interop handlers
21 | .BuildServiceProvider()
22 | .RunBootsharp(); // initialize interop services
23 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "Backend/Backend.csproj", "{D67BDA79-2E0C-4C27-913B-8ED98A7C6A73}"
3 | EndProject
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Prime", "Backend.Prime/Backend.Prime.csproj", "{F758E072-5DA6-499E-9F09-922D2367B201}"
5 | EndProject
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.WASM", "Backend.WASM/Backend.WASM.csproj", "{44F39802-9F5D-4D7E-9F63-8BAD2E3155A2}"
7 | EndProject
8 | Global
9 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
10 | {F758E072-5DA6-499E-9F09-922D2367B201}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
11 | {F758E072-5DA6-499E-9F09-922D2367B201}.Debug|Any CPU.Build.0 = Debug|Any CPU
12 | {F758E072-5DA6-499E-9F09-922D2367B201}.Release|Any CPU.ActiveCfg = Release|Any CPU
13 | {F758E072-5DA6-499E-9F09-922D2367B201}.Release|Any CPU.Build.0 = Release|Any CPU
14 | {44F39802-9F5D-4D7E-9F63-8BAD2E3155A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {44F39802-9F5D-4D7E-9F63-8BAD2E3155A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {44F39802-9F5D-4D7E-9F63-8BAD2E3155A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {44F39802-9F5D-4D7E-9F63-8BAD2E3155A2}.Release|Any CPU.Build.0 = Release|Any CPU
18 | {D67BDA79-2E0C-4C27-913B-8ED98A7C6A73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {D67BDA79-2E0C-4C27-913B-8ED98A7C6A73}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {D67BDA79-2E0C-4C27-913B-8ED98A7C6A73}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {D67BDA79-2E0C-4C27-913B-8ED98A7C6A73}.Release|Any CPU.Build.0 = Release|Any CPU
22 | EndGlobalSection
23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
24 | Debug|Any CPU = Debug|Any CPU
25 | Release|Any CPU = Release|Any CPU
26 | EndGlobalSection
27 | EndGlobal
28 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend/Backend.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/samples/react/backend/Backend/IComputer.cs:
--------------------------------------------------------------------------------
1 | namespace Backend;
2 |
3 | // In the domain assembly we outline the contract of a computer service.
4 | // The specific implementation is in other assembly, so that
5 | // domain is not coupled with the details.
6 |
7 | public interface IComputer
8 | {
9 | void StartComputing ();
10 | void StopComputing ();
11 | bool IsComputing ();
12 | }
13 |
--------------------------------------------------------------------------------
/samples/react/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "type": "module",
4 | "main": "Backend.WASM/bin/backend/index.mjs",
5 | "types": "Backend.WASM/bin/backend/types/index.d.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/samples/react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Sample
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/samples/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "backend": "file:backend",
5 | "react": "^18.2.0",
6 | "react-dom": "^18.2.0"
7 | },
8 | "devDependencies": {
9 | "typescript": "^5.3.3",
10 | "@types/react": "^18.2.48",
11 | "@types/react-dom": "^18.2.18",
12 | "vite": "^5.0.12",
13 | "@vitejs/plugin-react-swc": "^3.5.0",
14 | "vitest": "^1.2.1",
15 | "@vitest/coverage-v8": "^1.2.1",
16 | "happy-dom": "^13.2.0",
17 | "@testing-library/react": "^14.1.2",
18 | "@testing-library/user-event": "^14.5.2",
19 | "eslint": "^8.56.0",
20 | "eslint-plugin-react": "^7.33.2",
21 | "eslint-plugin-react-hooks": "^4.6.0",
22 | "@typescript-eslint/eslint-plugin": "^6.19.0",
23 | "npm-check-updates": "^16.14.12"
24 | },
25 | "scripts": {
26 | "update": "ncu --interactive",
27 | "backend": "dotnet publish backend",
28 | "lint": "eslint src test",
29 | "test": "vitest run",
30 | "cover": "vitest run --coverage",
31 | "dev": "vite",
32 | "build": "vite build",
33 | "preview": "vite preview"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "eslint:recommended",
38 | "plugin:react/recommended",
39 | "plugin:react/jsx-runtime",
40 | "plugin:react-hooks/recommended",
41 | "plugin:@typescript-eslint/recommended"
42 | ],
43 | "rules": { "react/display-name": "off" },
44 | "settings": { "react": { "version": "detect" } },
45 | "env": { "browser": true }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/samples/react/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
23 |
--------------------------------------------------------------------------------
/samples/react/src/donut.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | type Props = {
4 | fps: number;
5 | };
6 |
7 | export default ({ fps }: Props) => {
8 | const [time, setTime] = useState();
9 |
10 | useEffect(() => {
11 | const delay = 1000 / fps;
12 | const handle = setInterval(() => setTime(Date.now()), delay);
13 | return () => clearInterval(handle);
14 | }, [fps]);
15 |
16 | return ;
17 | };
18 |
--------------------------------------------------------------------------------
/samples/react/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | height: 100%;
6 | font-family: monospace;
7 | background-color: #333333;
8 | color: #cccccc;
9 | }
10 |
11 | #donut {
12 | border: 100px solid wheat;
13 | border-bottom: 100px solid chocolate;
14 | border-radius: 100%;
15 | width: 100px;
16 | height: 100px;
17 | }
18 |
19 | #computer {
20 | position: absolute;
21 | top: 10px;
22 | width: 300px;
23 | }
24 |
25 | #controls {
26 | margin-top: 10px;
27 | }
28 |
29 | #controls input {
30 | max-width: 60px;
31 | margin-left: 7px;
32 | margin-right: 15px;
33 | }
34 |
35 | #computer button {
36 | margin-top: 10px;
37 | width: 100%;
38 | }
39 |
40 | #results {
41 | margin-top: 10px;
42 | white-space: pre-wrap;
43 | opacity: 0.65;
44 | }
45 |
--------------------------------------------------------------------------------
/samples/react/src/main.tsx:
--------------------------------------------------------------------------------
1 | import backend from "backend";
2 | import react from "react-dom/client";
3 | import Computer from "./computer";
4 | import Donut from "./donut";
5 | import "./index.css";
6 |
7 | await backend.boot({ root: "/bin" });
8 |
9 | react.createRoot(document.getElementById("app")!).render(<>
10 |
11 |
12 | >);
13 |
--------------------------------------------------------------------------------
/samples/react/test/computer.test.tsx:
--------------------------------------------------------------------------------
1 | import { Computer as Backend } from "backend";
2 | import { beforeEach, test, expect, vi } from "vitest";
3 | import { render, act, screen } from "@testing-library/react";
4 | import userEvent from "@testing-library/user-event";
5 | import Computer from "../src/computer";
6 |
7 | beforeEach(() => {
8 | Backend.startComputing = vi.fn();
9 | Backend.stopComputing = vi.fn();
10 | Backend.isComputing = vi.fn(() => false);
11 | });
12 |
13 | test("not computing initially", () => {
14 | render();
15 | expect(Backend.startComputing).not.toHaveBeenCalled();
16 | });
17 |
18 | test("get options returns values specified in props", async () => {
19 | render();
20 | expect(Backend.getOptions()).toStrictEqual({ complexity: 666, multithreading: true });
21 | });
22 |
23 | test("compute time is written to screen", async () => {
24 | render();
25 | act(() => Backend.onComplete.broadcast(BigInt(13)));
26 | expect(screen.getByText(/Computed in 13ms/));
27 | });
28 |
29 | test("button click stops computing when running", async () => {
30 | Backend.isComputing = () => true;
31 | render();
32 | await userEvent.click(screen.getByRole("button"));
33 | expect(Backend.stopComputing).toHaveBeenCalled();
34 | });
35 |
36 | test("button click starts computing when not running", async () => {
37 | Backend.isComputing = () => false;
38 | render();
39 | await userEvent.click(screen.getByRole("button"));
40 | expect(Backend.startComputing).toHaveBeenCalled();
41 | });
42 |
--------------------------------------------------------------------------------
/samples/react/test/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from "vitest";
2 | import { cleanup } from "@testing-library/react";
3 |
4 | afterEach(cleanup);
5 |
--------------------------------------------------------------------------------
/samples/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "bundler",
6 | "jsx": "react-jsx",
7 | "strict": true
8 | },
9 | "include": ["src"]
10 | }
11 |
--------------------------------------------------------------------------------
/samples/react/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from "vite";
4 | import react from "@vitejs/plugin-react-swc";
5 |
6 | export default defineConfig({
7 | plugins: [react()],
8 | test: {
9 | environment: "happy-dom",
10 | setupFiles: ["test/setup.ts"],
11 | coverage: { include: ["src/**"] }
12 | },
13 | server: {
14 | headers: {
15 | "Cross-Origin-Opener-Policy": "same-origin",
16 | "Cross-Origin-Embedder-Policy": "require-corp"
17 | }
18 | },
19 | build: {
20 | target: "chrome89",
21 | // Ignore node-specific calls in .NET's JavaScript:
22 | // https://github.com/dotnet/runtime/issues/91558.
23 | rollupOptions: { external: ["process", "module"] }
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/samples/trimming/README.md:
--------------------------------------------------------------------------------
1 | Example on producing minimal possible build size by disabling binaries embedding and utilizing aggressive trimming.
2 |
3 | To test and measure build size:
4 | - Run `dotnet publish cs`;
5 | - Run `node main.mjs`.
6 |
7 | ### Measurements (KB)
8 |
9 | | | Raw | Brotli |
10 | |-------------|-------|--------|
11 | | .NET 8 | 2,298 | 739 |
12 | | .NET 9 LLVM | 1,737 | 518 |
13 |
--------------------------------------------------------------------------------
/samples/trimming/cs/Program.cs:
--------------------------------------------------------------------------------
1 | using Bootsharp;
2 |
3 | public static partial class Program
4 | {
5 | public static void Main ()
6 | {
7 | Log("Hello from .NET!");
8 | }
9 |
10 | [JSFunction]
11 | public static partial void Log (string message);
12 | }
13 |
--------------------------------------------------------------------------------
/samples/trimming/main.mjs:
--------------------------------------------------------------------------------
1 | import bootsharp, { Program } from "./cs/bin/bootsharp/index.mjs";
2 | import { pathToFileURL } from "node:url";
3 | import fs from "node:fs/promises";
4 | import zlib from "node:zlib";
5 | import util from "node:util";
6 | import path from "node:path";
7 |
8 | console.log(`Binary size: ${await measure("./cs/bin/bootsharp/bin")}KB`);
9 | console.log(`Brotli size: ${await measure("./cs/bin/bootsharp/bro")}KB`);
10 |
11 | const resources = { ...bootsharp.resources };
12 | await Promise.all([
13 | fetchBro(resources.wasm),
14 | ...resources.assemblies.map(fetchBro)
15 | ]);
16 |
17 | Program.log = console.log;
18 | const root = pathToFileURL(path.resolve("./cs/bin/bootsharp/bin"));
19 | await bootsharp.boot({ root, resources });
20 |
21 | async function measure(dir) {
22 | let size = 0;
23 | for await (const entry of await fs.opendir(dir))
24 | size += (await fs.stat(`${entry.path}/${entry.name}`)).size;
25 | return Math.ceil(size / 1024);
26 | }
27 |
28 | async function fetchBro(resource) {
29 | const bro = await fs.readFile(`./cs/bin/bootsharp/bro/${resource.name}.br`);
30 | resource.content = await util.promisify(zlib.brotliDecompress)(bro);
31 | }
32 |
--------------------------------------------------------------------------------
/samples/vscode/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Artyom Sovetnikov (Elringus)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/samples/vscode/README.md:
--------------------------------------------------------------------------------
1 | An example on using [Bootsharp](https://github.com/Elringus/Bootsharp)-compiled C# solution inside VS Code standalone (node) and [web](https://code.visualstudio.com/api/extension-guides/web-extensions) extensions.
2 |
3 | Install [Hello, Bootsharp!](https://marketplace.visualstudio.com/items?itemName=Elringus.bootsharp) extension in VS Code, open command palette (`Ctrl+Shift+P` on standalone or `F1` on web) and execute `Hello, Bootsharp!` command. If successful, a welcome notification will appear.
4 |
5 | 
6 |
7 | To test the extension locally:
8 |
9 | - Run `npm run build` to build the sources;
10 | - Run `npm run package` to bundle extension package;
11 | - Drag-drop the produced `.vsix` file to the VS Code extension tab.
12 |
--------------------------------------------------------------------------------
/samples/vscode/assets/package-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elringus/bootsharp/2e96317910d798d162c2fc6e49bdd1443b7eea31/samples/vscode/assets/package-icon.png
--------------------------------------------------------------------------------
/samples/vscode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bootsharp",
3 | "version": "0.0.2",
4 | "displayName": "Hello, Bootsharp!",
5 | "description": "A test web extension built with Bootsharp.",
6 | "categories": [
7 | "Other"
8 | ],
9 | "publisher": "Elringus",
10 | "repository": "https://github.com/Elringus/Bootsharp",
11 | "homepage": "https://bootsharp.com",
12 | "icon": "assets/package-icon.png",
13 | "engines": {
14 | "vscode": "^1.81.1"
15 | },
16 | "browser": "./dist/extension.js",
17 | "activationEvents": [
18 | "onCommand:bootsharp.hello"
19 | ],
20 | "contributes": {
21 | "commands": [
22 | {
23 | "command": "bootsharp.hello",
24 | "title": "Hello, Bootsharp!",
25 | "category": "Bootsharp"
26 | }
27 | ]
28 | },
29 | "scripts": {
30 | "build": "rollup src/extension.js -o dist/extension.js -f cjs -g process,module,vscode",
31 | "package": "vsce package"
32 | },
33 | "devDependencies": {
34 | "@vscode/vsce": "^2.21.0",
35 | "rollup": "^3.28.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/samples/vscode/src/extension.js:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import bootsharp, { Program } from "../../Minimal/cs/bin/bootsharp/index.mjs";
3 |
4 | export async function activate(context) {
5 | Program.getFrontendName = () => "VS Code";
6 | try { await bootsharp.boot(); }
7 | catch (e) { vscode.window.showErrorMessage(e.message); }
8 | const command = vscode.commands.registerCommand("bootsharp.hello", greet);
9 | context.subscriptions.push(command);
10 | }
11 |
12 | export function deactivate() {
13 | bootsharp.exit();
14 | }
15 |
16 | function greet() {
17 | const message = `Welcome, ${Program.getBackendName()}! Enjoy your VS Code extension space.`;
18 | vscode.window.showInformationMessage(message);
19 | }
20 |
--------------------------------------------------------------------------------
/src/cs/.scripts/cover.ps1:
--------------------------------------------------------------------------------
1 | try {
2 | $out = "../.cover/"
3 | $json = "../.cover/coverage.json"
4 | dotnet test Bootsharp.Common.Test/Bootsharp.Common.Test.csproj /p:CollectCoverage=true /p:ExcludeByAttribute=GeneratedCodeAttribute /p:CoverletOutput=$out
5 | dotnet test Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj /p:CollectCoverage=true /p:ExcludeByAttribute=GeneratedCodeAttribute /p:CoverletOutput=$out /p:MergeWith=$json
6 | dotnet test Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj /p:CollectCoverage=true /p:ExcludeByAttribute=GeneratedCodeAttribute /p:CoverletOutput=$out /p:MergeWith=$json
7 | dotnet test Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj /p:CollectCoverage=true /p:CoverletOutputFormat="json%2copencover" /p:ExcludeByAttribute=GeneratedCodeAttribute /p:CoverletOutput=$out /p:MergeWith=$json
8 | reportgenerator "-reports:*/*.xml" "-targetdir:.cover" -reporttypes:HTML
9 | python -m webbrowser http://localhost:3000
10 | npx serve .cover
11 | } finally {
12 | rm .cover -r -force
13 | }
14 |
--------------------------------------------------------------------------------
/src/cs/.scripts/pack.ps1:
--------------------------------------------------------------------------------
1 | dotnet build Bootsharp.Generate -c Release
2 | dotnet pack Bootsharp.Common -o .nuget -c Release
3 | dotnet pack Bootsharp.Inject -o .nuget -c Release
4 | dotnet pack Bootsharp -o .nuget -c Release
5 | dotnet restore
6 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 | all
23 |
24 |
25 | runtime; build; native; contentfiles; analyzers; buildtransitive
26 | all
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/InstancesTest.cs:
--------------------------------------------------------------------------------
1 | using static Bootsharp.Instances;
2 |
3 | namespace Bootsharp.Common.Test;
4 |
5 | public class InstancesTest
6 | {
7 | [Fact]
8 | public void ThrowsWhenGettingUnregisteredInstance ()
9 | {
10 | Assert.Throws(() => Get(0));
11 | }
12 |
13 | [Fact]
14 | public void ThrowsWhenDisposingUnregisteredInstance ()
15 | {
16 | Assert.Throws(() => Dispose(0));
17 | }
18 |
19 | [Fact]
20 | public void CanRegisterGetAndDisposeInstance ()
21 | {
22 | var instance = new object();
23 | var id = Register(instance);
24 | Assert.Same(instance, Get(id));
25 | Dispose(id);
26 | Assert.Throws(() => Get(id));
27 | }
28 |
29 | [Fact]
30 | public void GeneratesUniqueIdsOnEachRegister ()
31 | {
32 | Assert.NotEqual(Register(new object()), Register(new object()));
33 | }
34 |
35 | [Fact]
36 | public void ReusesIdOfDisposedInstance ()
37 | {
38 | var id = Register(new object());
39 | Dispose(id);
40 | Assert.Equal(id, Register(new object()));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/InterfacesTest.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp.Common.Test;
2 |
3 | public class InterfacesTest
4 | {
5 | [Fact]
6 | public void RegistersExports ()
7 | {
8 | var export = new ExportInterface(typeof(IBackend), null);
9 | Interfaces.Register(typeof(Backend), export);
10 | Assert.Equal(typeof(IBackend), Interfaces.Exports[typeof(Backend)].Interface);
11 | }
12 |
13 | [Fact]
14 | public void RegistersImports ()
15 | {
16 | var import = new ImportInterface(new Frontend());
17 | Interfaces.Register(typeof(IFrontend), import);
18 | Assert.IsType(Interfaces.Imports[typeof(IFrontend)].Instance);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/Mocks.cs:
--------------------------------------------------------------------------------
1 | global using static Bootsharp.Common.Test.Mocks;
2 |
3 | namespace Bootsharp.Common.Test;
4 |
5 | public static class Mocks
6 | {
7 | public interface IBackend;
8 | public interface IFrontend;
9 | public class Backend : IBackend;
10 | public class Frontend : IFrontend;
11 |
12 | public record MockItem (string Id);
13 | public record MockRecord (IReadOnlyList Items);
14 | }
15 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/ProxiesTest.cs:
--------------------------------------------------------------------------------
1 | using static Bootsharp.Proxies;
2 |
3 | namespace Bootsharp.Common.Test;
4 |
5 | public class ProxiesTest
6 | {
7 | [Fact]
8 | public void WhenEndpointNotFoundErrorIsThrown ()
9 | {
10 | Assert.Contains("Proxy 'foo' is not found.",
11 | Assert.Throws(() => Get("foo")).Message);
12 | }
13 |
14 | [Fact]
15 | public void WhenFunctionTypeIsWrongErrorIsThrown ()
16 | {
17 | Set("bar", null);
18 | Assert.Contains("Proxy 'bar' is not 'System.Action'.",
19 | Assert.Throws(() => Get("bar")).Message);
20 | }
21 |
22 | [Fact]
23 | public void CanSetAndGetDelegate ()
24 | {
25 | Set("echo", (int x, int y) => x + y);
26 | Assert.Equal(15, Get>("echo")(6, 9));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/SerializerTest.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 | using static Bootsharp.Serializer;
4 |
5 | namespace Bootsharp.Common.Test;
6 |
7 | public partial class SerializerTest
8 | {
9 | [JsonSerializable(typeof(bool))]
10 | [JsonSerializable(typeof(int?))]
11 | [JsonSerializable(typeof(MockItem))]
12 | [JsonSerializable(typeof(MockRecord))]
13 | [JsonSourceGenerationOptions(
14 | PropertyNameCaseInsensitive = true,
15 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
16 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
17 | )]
18 | internal partial class SerializerContext : JsonSerializerContext;
19 |
20 | [Fact]
21 | public void CanSerialize ()
22 | {
23 | Assert.Equal("""{"items":[{"id":"foo"},{"id":"bar"}]}""",
24 | Serialize(new MockRecord([new("foo"), new("bar")]), SerializerContext.Default.MockRecord));
25 | }
26 |
27 | [Fact]
28 | public void SerializesNullAsNull ()
29 | {
30 | Assert.Equal("null", Serialize(null, SerializerContext.Default.MockRecord));
31 | }
32 |
33 | [Fact]
34 | public void CanDeserialize ()
35 | {
36 | Assert.Equal([new("foo"), new("bar")],
37 | Deserialize("""{"items":[{"id":"foo"},{"id":"bar"}]}""", SerializerContext.Default.MockRecord).Items);
38 | }
39 |
40 | [Fact]
41 | public void DeserializesNullAndUndefinedAsDefault ()
42 | {
43 | Assert.Null(Deserialize(null, SerializerContext.Default.MockItem));
44 | Assert.Null(Deserialize("null", SerializerContext.Default.MockItem));
45 | Assert.Null(Deserialize("undefined", SerializerContext.Default.MockItem));
46 | Assert.Null(Deserialize(null, SerializerContext.Default.NullableInt32));
47 | Assert.Null(Deserialize("null", SerializerContext.Default.NullableInt32));
48 | Assert.Null(Deserialize("undefined", SerializerContext.Default.NullableInt32));
49 | Assert.False(Deserialize(null, SerializerContext.Default.Boolean));
50 | Assert.False(Deserialize("null", SerializerContext.Default.Boolean));
51 | Assert.False(Deserialize("undefined", SerializerContext.Default.Boolean));
52 | }
53 |
54 | [Fact]
55 | public void WhenDeserializationFailsErrorIsThrown ()
56 | {
57 | Assert.Throws(() => Deserialize("", SerializerContext.Default.Int32));
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common.Test/TypesTest.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Bootsharp;
3 |
4 | [assembly: JSExport(typeof(IBackend))]
5 | [assembly: JSImport(typeof(IFrontend))]
6 |
7 | namespace Bootsharp.Common.Test;
8 |
9 | public class TypesTest
10 | {
11 | private readonly CustomAttributeData export = GetMockExportAttribute();
12 | private readonly CustomAttributeData import = GetMockImportAttribute();
13 |
14 | [Fact]
15 | public void TypesAreAssigned ()
16 | {
17 | Assert.Equal([typeof(IBackend)], new JSExportAttribute(typeof(IBackend)).Types);
18 | Assert.Equal([typeof(IFrontend)], new JSImportAttribute(typeof(IFrontend)).Types);
19 | Assert.Equal("Space", (new JSPreferencesAttribute { Space = ["Space"] }).Space[0]);
20 | }
21 |
22 | [Fact]
23 | public void ExportParametersEqualArguments ()
24 | {
25 | Assert.Equal([typeof(IBackend)],
26 | (export.ConstructorArguments[0].Value as IReadOnlyCollection).Select(a => a.Value));
27 | }
28 |
29 | [Fact]
30 | public void ImportParametersEqualArguments ()
31 | {
32 | Assert.Equal([typeof(IFrontend)],
33 | (import.ConstructorArguments[0].Value as IReadOnlyCollection).Select(a => a.Value));
34 | }
35 |
36 | private static CustomAttributeData GetMockExportAttribute () =>
37 | typeof(TypesTest).Assembly.CustomAttributes
38 | .First(a => a.AttributeType == typeof(JSExportAttribute));
39 | private static CustomAttributeData GetMockImportAttribute () =>
40 | typeof(TypesTest).Assembly.CustomAttributes
41 | .First(a => a.AttributeType == typeof(JSImportAttribute));
42 | }
43 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Attributes/JSEventAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// Applied to a partial method to bind it with an event meant to be
5 | /// broadcast (invoked) in C# and subscribed (listened) to in JavaScript.
6 | ///
7 | ///
8 | ///
9 | /// [JSEvent]
10 | /// public static partial void OnSomethingHappened (string payload);
11 | /// Namespace.onSomethingHappened.subscribe(payload => ...);
12 | ///
13 | ///
14 | [AttributeUsage(AttributeTargets.Method)]
15 | public sealed class JSEventAttribute : Attribute;
16 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Attributes/JSExportAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// When applied to WASM entry point assembly, specified interfaces will
5 | /// be automatically exported for consumption on JavaScript side.
6 | ///
7 | ///
8 | /// Generated bindings have to be initialized with the handler implementation.
9 | /// For example, given "IHandler" interface is exported, "JSHandler" class will be generated,
10 | /// which has to be instantiated with an "IHandler" implementation instance.
11 | ///
12 | ///
13 | /// Expose "IHandlerA" and "IHandlerB" C# APIs to JavaScript and wrap invocations in "Utils.Try()":
14 | ///
15 | /// [assembly: JSExport(
16 | /// typeof(IHandlerA),
17 | /// typeof(IHandlerB),
18 | /// invokePattern = "(.+)",
19 | /// invokeReplacement = "Utils.Try(() => $1)"
20 | /// )]
21 | ///
22 | ///
23 | [AttributeUsage(AttributeTargets.Assembly)]
24 | public sealed class JSExportAttribute : Attribute
25 | {
26 | ///
27 | /// The interface types to generated export bindings for.
28 | ///
29 | public Type[] Types { get; }
30 |
31 | /// The interface types to generate export bindings for.
32 | public JSExportAttribute (params Type[] types)
33 | {
34 | Types = types;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Attributes/JSFunctionAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// Applied to a partial method to bind it with a JavaScript function.
5 | ///
6 | ///
7 | /// The implementation is expected to be assigned as "Namespace.method = function".
8 | ///
9 | ///
10 | ///
11 | /// [JSFunction]
12 | /// public static partial string GetHostName ();
13 | /// Namespace.getHostName = () => "Browser";
14 | ///
15 | ///
16 | [AttributeUsage(AttributeTargets.Method)]
17 | public sealed class JSFunctionAttribute : Attribute;
18 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Attributes/JSImportAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// When applied to WASM entry point assembly, JavaScript bindings for the specified interfaces
5 | /// will be automatically generated for consumption on C# side.
6 | ///
7 | ///
8 | /// Generated bindings have to be implemented on JavaScript side.
9 | /// For example, given "IFrontend" interface is imported, "JSFrontend" class will be generated,
10 | /// which has to be implemented in JavaScript.
11 | /// When an interface method starts with "Notify", an event bindings will ge generated (instead of function);
12 | /// JavaScript name of the event will start with "on" instead of "Notify".
13 | /// This behaviour can be configured via preferences.
14 | ///
15 | ///
16 | /// Generate JavaScript APIs based on "IFrontendAPI" and "IOtherFrontendAPI" interfaces:
17 | ///
18 | /// [assembly: JSImport(
19 | /// typeof(IFrontendAPI),
20 | /// typeof(IOtherFrontendAPI)
21 | /// )]
22 | ///
23 | ///
24 | [AttributeUsage(AttributeTargets.Assembly)]
25 | public sealed class JSImportAttribute : Attribute
26 | {
27 | ///
28 | /// The interface types to generated import bindings for.
29 | ///
30 | public Type[] Types { get; }
31 |
32 | /// The interface types to generate import bindings for.
33 | public JSImportAttribute (params Type[] types)
34 | {
35 | Types = types;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Attributes/JSInvokableAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// Applied to a static method to make it invokable in JavaScript.
5 | ///
6 | ///
7 | ///
8 | /// [JSInvokable]
9 | /// public static string GetName () => "Sharp";
10 | /// console.log(Namespace.getName());
11 | ///
12 | ///
13 | [AttributeUsage(AttributeTargets.Method)]
14 | public sealed class JSInvokableAttribute : Attribute;
15 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Attributes/JSPreferencesAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// When applied to WASM entry point assembly, configures Bootsharp behaviour at build time.
5 | ///
6 | ///
7 | /// Each attribute property expects array of pattern and replacement string pairs, which are
8 | /// supplied to Regex.Replace when generating associated JavaScript code. Each consequent pair
9 | /// is tested in order; on first match the result replaces the default.
10 | ///
11 | ///
12 | /// Make all spaces starting with "Foo.Bar" replaced with "Baz":
13 | ///
14 | /// [assembly: Bootsharp.JSPreferences(
15 | /// Space = ["^Foo\.Bar\.(\S+)", "Baz.$1"]
16 | /// )]
17 | ///
18 | ///
19 | [AttributeUsage(AttributeTargets.Assembly)]
20 | public sealed class JSPreferencesAttribute : Attribute
21 | {
22 | ///
23 | /// Customize generated JavaScript object names and TypeScript namespaces.
24 | ///
25 | ///
26 | /// The patterns are matched against full type name (namespace.typename) of
27 | /// declaring C# type when generating JavaScript objects for interop methods
28 | /// and against namespace when generating TypeScript syntax for C# types.
29 | /// Matched type names have following modifications:
30 | /// - interfaces have first character removed
31 | /// - generics have parameter spec removed
32 | /// - nested have "+" replaced with "."
33 | ///
34 | public string[] Space { get; init; } = [];
35 | ///
36 | /// Customize generated TypeScript type syntax.
37 | ///
38 | ///
39 | /// The patterns are matched against full C# type names of
40 | /// interop method arguments, return values and object properties.
41 | ///
42 | public string[] Type { get; init; } = [];
43 | ///
44 | /// Customize which C# methods should be transformed into JavaScript
45 | /// events, as well as generated event names.
46 | ///
47 | ///
48 | /// The patterns are matched against C# method names declared under
49 | /// interfaces. By default, methods
50 | /// starting with "Notify.." are matched.
51 | ///
52 | public string[] Event { get; init; } = [];
53 | ///
54 | /// Customize generated JavaScript function names.
55 | ///
56 | ///
57 | /// The patterns are matched against C# interop method names.
58 | ///
59 | public string[] Function { get; init; } = [];
60 | }
61 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Bootsharp.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | Bootsharp.Common
8 | Bootsharp.Common
9 | Platform-agnostic common Bootsharp APIs.
10 | true
11 | CS1591
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Error.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// Exception thrown from Bootsharp internal behaviour.
5 | ///
6 | public sealed class Error (string message) : Exception(message);
7 |
--------------------------------------------------------------------------------
/src/cs/Bootsharp.Common/Interop/ExportInterface.cs:
--------------------------------------------------------------------------------
1 | namespace Bootsharp;
2 |
3 | ///
4 | /// Metadata about generated interop class for an interface supplied
5 | /// under .
6 | ///
7 | /// Type of the exported interface.
8 | /// Takes export interface implementation instance; returns interop class instance.
9 | public record ExportInterface (Type Interface, Func