├── .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 | Bootsharp 4 | 5 |

6 |
7 |

8 | nuget 9 | codefactor 10 | codecov 11 | codeql 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 | ![](https://raw.githubusercontent.com/elringus/bootsharp/main/docs/public/img/banner.png) 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 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 | ![](https://i.gyazo.com/a3ec0ee51f14970a7eca24169d682274.png) 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 Factory); 10 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Common/Interop/ImportInterface.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp; 2 | 3 | /// 4 | /// Metadata about generated implementation for interface supplied 5 | /// under . 6 | /// 7 | /// Import interface implementation instance. 8 | public record ImportInterface (object Instance); 9 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Common/Interop/Instances.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp; 2 | 3 | /// 4 | /// Manages exported (C# -> JavaScript) instanced interop interfaces. 5 | /// 6 | public static class Instances 7 | { 8 | private static readonly Dictionary idToInstance = []; 9 | private static readonly Queue idPool = []; 10 | private static int nextId = int.MinValue; 11 | 12 | /// 13 | /// Registers specified interop instance and associates it with unique ID. 14 | /// 15 | /// The instance to register. 16 | /// Unique ID associated with the registered instance. 17 | public static int Register (object instance) 18 | { 19 | var id = idPool.Count > 0 ? idPool.Dequeue() : nextId++; 20 | idToInstance[id] = instance; 21 | return id; 22 | } 23 | 24 | /// 25 | /// Resolves registered instance by the specified ID. 26 | /// 27 | /// Unique ID of the instance to resolve. 28 | public static object Get (int id) 29 | { 30 | if (!idToInstance.TryGetValue(id, out var instance)) 31 | throw new Error($"Failed to resolve exported interop instance with '{id}' ID: not registered."); 32 | return instance; 33 | } 34 | 35 | /// 36 | /// Notifies that interop instance is no longer used on JavaScript side 37 | /// (eg, was garbage collected) and can be released on C# side as well. 38 | /// 39 | /// ID of the disposed interop instance. 40 | public static void Dispose (int id) 41 | { 42 | if (!idToInstance.Remove(id)) 43 | throw new Error($"Failed to dispose exported interop instance with '{id}' ID: not registered."); 44 | idPool.Enqueue(id); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Common/Interop/Interfaces.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Bootsharp; 4 | 5 | /// 6 | /// Provides access to generated interop types for interfaces supplied 7 | /// under and . 8 | /// 9 | /// 10 | /// Exported interfaces are C# APIs invoked in JavaScript. Their C# implementation 11 | /// (handler) is assumed to be supplied via 12 | /// on program boot (usually via DI), before associated APIs are accessed in JavaScript. 13 | /// Imported interfaces are JavaScript APIs invoked in C#. Their implementation 14 | /// is instantiated in generated code and is available before program start. 15 | /// 16 | [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] 17 | public static class Interfaces 18 | { 19 | /// 20 | /// Interop classes generated for interfaces 21 | /// mapped by the generated class type. Expected to have 22 | /// invoked with the interface implementation (handler) before associated API usage in JS. 23 | /// 24 | public static IReadOnlyDictionary Exports => exports; 25 | /// 26 | /// Implementations generated for interop 27 | /// interfaces mapped by the interface type of the associated implementation. 28 | /// 29 | public static IReadOnlyDictionary Imports => imports; 30 | 31 | private static readonly Dictionary exports = new(); 32 | private static readonly Dictionary imports = new(); 33 | 34 | /// 35 | /// Maps type of the generated export interop class to the associated metadata. 36 | /// Invoked by the generated code before program start. 37 | /// 38 | public static void Register (Type @class, ExportInterface export) => exports[@class] = export; 39 | /// 40 | /// Maps interface type of the generated import implementation to the associated metadata. 41 | /// Invoked by the generated code before program start. 42 | /// 43 | public static void Register (Type @interface, ImportInterface import) => imports[@interface] = import; 44 | } 45 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Common/Interop/Proxies.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp; 2 | 3 | /// 4 | /// Provides access to generated interop methods for JavaScript functions and events. 5 | /// 6 | /// 7 | /// Below is for internal reference; end users are not expected to use this API.
8 | /// Partial interop methods ( and ) 9 | /// are accessed via delegates registered by associated IDs, where ID is the full name of 10 | /// the declaring type of the interop method joined with the method's name by a dot. Eg, given:
11 | /// 12 | /// namespace Space; 13 | /// public static partial class Class 14 | /// { 15 | /// [JSFunction] public static partial int Foo (string arg); 16 | /// } 17 | ///
18 | /// Proxy for the "Foo" method is registered as follows (emitted at build; 19 | /// actual code will have additional de-/serialization steps):
20 | /// 21 | /// Proxies.Set("Space.Class.Foo", (arg) => Bootsharp.Generated.Interop.Space_Class_Foo(arg)); 22 | ///
23 | /// Registered proxy is accessed as follows (emitted by source generator to 24 | /// implement original partial "Foo" method):
25 | /// 26 | /// public static int Foo (string arg) => >("Space.Class.Foo")(arg);]]> 27 | ///
28 | ///
29 | public static class Proxies 30 | { 31 | private static readonly Dictionary map = new(StringComparer.Ordinal); 32 | 33 | /// 34 | /// Maps specified interop delegate to the specified ID. 35 | /// 36 | /// 37 | /// Performed in the generated interop code at module initialization. 38 | /// 39 | public static void Set (string id, Delegate @delegate) 40 | { 41 | map[id] = @delegate; 42 | } 43 | 44 | /// 45 | /// Returns interop delegate of specified ID and type. 46 | /// 47 | /// 48 | /// Used in sources generated for partial 49 | /// and methods. 50 | /// 51 | public static T Get (string id) where T : Delegate 52 | { 53 | if (!map.TryGetValue(id, out var @delegate)) 54 | throw new Error($"Proxy '{id}' is not found."); 55 | if (@delegate is not T specific) 56 | throw new Error($"Proxy '{id}' is not '{typeof(T)}'."); 57 | return specific; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Common/Interop/Serializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization.Metadata; 3 | 4 | namespace Bootsharp; 5 | 6 | /// 7 | /// Handles serialization of the interop values that can't be passed to and from JavaScript as-is. 8 | /// 9 | public static class Serializer 10 | { 11 | /// 12 | /// Serializes specified object to JSON string using specified serialization context info. 13 | /// 14 | public static string Serialize (T? @object, JsonTypeInfo info) 15 | { 16 | if (@object is null) return "null"; 17 | return JsonSerializer.Serialize(@object, info); 18 | } 19 | 20 | /// 21 | /// Deserializes specified JSON string to the object of specified type. 22 | /// 23 | public static T? Deserialize (string? json, JsonTypeInfo info) 24 | { 25 | if (json is null || 26 | json.Equals("null", StringComparison.Ordinal) || 27 | json.Equals("undefined", StringComparison.Ordinal)) return default; 28 | return JsonSerializer.Deserialize(json, info); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate.Test/EventTest.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Generate.Test; 2 | 3 | public static class EventTest 4 | { 5 | public static object[][] Data { get; } = [ 6 | // Can generate event binding without namespace and arguments. 7 | [ 8 | """ 9 | partial class Foo 10 | { 11 | [JSEvent] partial void OnBar (); 12 | } 13 | """, 14 | """ 15 | partial class Foo 16 | { 17 | partial void OnBar () => 18 | #if BOOTSHARP_EMITTED 19 | global::Bootsharp.Generated.Interop.Proxy_Foo_OnBar(); 20 | #else 21 | throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); 22 | #endif 23 | } 24 | """ 25 | ], 26 | // Can generate event binding with namespace and arguments. 27 | [ 28 | """ 29 | namespace Space; 30 | 31 | public static partial class Foo 32 | { 33 | [JSEvent] public static partial void OnBar (string a, int b); 34 | } 35 | """, 36 | """ 37 | namespace Space; 38 | 39 | public static partial class Foo 40 | { 41 | public static partial void OnBar (global::System.String a, global::System.Int32 b) => 42 | #if BOOTSHARP_EMITTED 43 | global::Bootsharp.Generated.Interop.Proxy_Space_Foo_OnBar(a, b); 44 | #else 45 | throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); 46 | #endif 47 | } 48 | """ 49 | ] 50 | ]; 51 | } 52 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate.Test/Verifier.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Testing; 3 | using Microsoft.CodeAnalysis.Testing; 4 | 5 | namespace Bootsharp.Generate.Test; 6 | 7 | public sealed class Verifier : CSharpSourceGeneratorTest 8 | where T : IIncrementalGenerator, new() 9 | { 10 | protected override string DefaultTestProjectName { get; } = "GeneratorTest"; 11 | 12 | public Verifier () => ReferenceAssemblies = ReferenceAssemblies.Net.Net80; 13 | 14 | protected override bool IsCompilerDiagnosticIncluded (Diagnostic diagnostic, CompilerDiagnostics _) => 15 | diagnostic.Severity == DiagnosticSeverity.Error; 16 | } 17 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | enable 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate/PartialClass.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace Bootsharp.Generate; 5 | 6 | internal sealed class PartialClass ( 7 | Compilation compilation, 8 | ClassDeclarationSyntax syntax, 9 | IReadOnlyList methods) 10 | { 11 | public string Name { get; } = syntax.Identifier.ToString(); 12 | 13 | public string EmitSource () => 14 | """ 15 | #nullable enable 16 | #pragma warning disable 17 | 18 | """ + 19 | EmitUsings() + 20 | WrapNamespace( 21 | EmitHeader() + 22 | EmitMethods() + 23 | EmitFooter() 24 | ); 25 | 26 | private string EmitUsings () 27 | { 28 | var usings = string.Join("\n", syntax.SyntaxTree.GetRoot() 29 | .DescendantNodesAndSelf().OfType()); 30 | return string.IsNullOrEmpty(usings) ? "" : usings + "\n\n"; 31 | } 32 | 33 | private string EmitHeader () => $"{syntax.Modifiers} class {syntax.Identifier}\n{{"; 34 | 35 | private string EmitMethods () 36 | { 37 | var sources = methods.Select(m => " " + m.EmitSource(compilation)); 38 | return "\n" + string.Join("\n", sources); 39 | } 40 | 41 | private string EmitFooter () => "\n}"; 42 | 43 | private string WrapNamespace (string source) 44 | { 45 | if (syntax.Parent is NamespaceDeclarationSyntax space) 46 | return $$""" 47 | namespace {{space.Name}} 48 | { 49 | {{string.Join("\n", source.Split(["\r\n", "\r", "\n"], StringSplitOptions.None) 50 | .Select((s, i) => i > 0 && s.Length > 0 ? " " + s : s))}} 51 | } 52 | """; 53 | if (syntax.Parent is FileScopedNamespaceDeclarationSyntax fileSpace) 54 | return $"namespace {fileSpace.Name};\n\n{source}"; 55 | return source; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate/SourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Bootsharp.Generate; 4 | 5 | [Generator(LanguageNames.CSharp)] 6 | public sealed class SourceGenerator : IIncrementalGenerator 7 | { 8 | public void Initialize (IncrementalGeneratorInitializationContext context) => context 9 | .RegisterSourceOutput(context.CompilationProvider, Compile); 10 | 11 | private static void Compile (SourceProductionContext context, Compilation compilation) 12 | { 13 | var receiver = VisitNodes(compilation); 14 | foreach (var @class in receiver.FunctionClasses) 15 | context.AddSource($"{@class.Name}Functions.g", @class.EmitSource()); 16 | foreach (var @class in receiver.EventClasses) 17 | context.AddSource($"{@class.Name}Events.g", @class.EmitSource()); 18 | } 19 | 20 | private static SyntaxReceiver VisitNodes (Compilation compilation) 21 | { 22 | var receiver = new SyntaxReceiver(); 23 | foreach (var tree in compilation.SyntaxTrees) 24 | if (!tree.FilePath.EndsWith(".g.cs")) 25 | foreach (var node in tree.GetRoot().DescendantNodesAndSelf()) 26 | receiver.VisitNode(node, compilation); 27 | return receiver; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Generate/SyntaxReceiver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace Bootsharp.Generate; 5 | 6 | internal sealed class SyntaxReceiver 7 | { 8 | public List FunctionClasses { get; } = []; 9 | public List EventClasses { get; } = []; 10 | 11 | public void VisitNode (SyntaxNode node, Compilation compilation) 12 | { 13 | if (node is ClassDeclarationSyntax classSyntax) 14 | VisitClass(classSyntax, compilation); 15 | } 16 | 17 | private void VisitClass (ClassDeclarationSyntax syntax, Compilation compilation) 18 | { 19 | var functions = GetMethodsWithAttribute(syntax, "JSFunction"); 20 | if (functions.Count > 0) FunctionClasses.Add(new(compilation, syntax, functions)); 21 | var events = GetMethodsWithAttribute(syntax, "JSEvent"); 22 | if (events.Count > 0) EventClasses.Add(new(compilation, syntax, events)); 23 | } 24 | 25 | private List GetMethodsWithAttribute (ClassDeclarationSyntax syntax, string attribute) 26 | { 27 | return syntax.Members 28 | .OfType() 29 | .Where(s => HasAttribute(s, attribute)) 30 | .Select(m => new PartialMethod(m)).ToList(); 31 | } 32 | 33 | private bool HasAttribute (MethodDeclarationSyntax syntax, string attributeName) 34 | { 35 | return syntax.AttributeLists 36 | .SelectMany(l => l.Attributes) 37 | .Any(a => a.ToString().Contains(attributeName)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Inject.Test/ExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Bootsharp.Inject.Test; 4 | 5 | public class TypesTest 6 | { 7 | [Fact] 8 | public void CanInjectGeneratedTypes () 9 | { 10 | AddAutogenerated(); 11 | var provider = new ServiceCollection() 12 | .AddBootsharp() 13 | .BuildServiceProvider(); 14 | Assert.IsType(provider.GetRequiredService()); 15 | } 16 | 17 | [Fact] 18 | public void CanInitializeGeneratedTypes () 19 | { 20 | AddAutogenerated(); 21 | new ServiceCollection() 22 | .AddSingleton() 23 | .AddBootsharp() 24 | .BuildServiceProvider() 25 | .RunBootsharp(); 26 | Assert.IsType(JSBackend.Handler); 27 | } 28 | 29 | [Fact] 30 | public void WhenMissingRequiredDependencyErrorIsThrown () 31 | { 32 | AddAutogenerated(); 33 | Assert.Contains("Failed to run Bootsharp: 'Bootsharp.Inject.Test.Mocks+IBackend' dependency is not registered.", 34 | Assert.Throws(() => new ServiceCollection().AddBootsharp().BuildServiceProvider().RunBootsharp()).Message); 35 | } 36 | 37 | // emulates auto-generated code behaviour on module initialization 38 | private static void AddAutogenerated () 39 | { 40 | Interfaces.Register(typeof(JSBackend), new ExportInterface(typeof(IBackend), h => new JSBackend((IBackend)h))); 41 | Interfaces.Register(typeof(IFrontend), new ImportInterface(new JSFrontend())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Inject.Test/Mocks.cs: -------------------------------------------------------------------------------- 1 | global using static Bootsharp.Inject.Test.Mocks; 2 | 3 | namespace Bootsharp.Inject.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 | public class JSFrontend : IFrontend; 12 | 13 | public class JSBackend 14 | { 15 | public static IBackend Handler { get; private set; } 16 | 17 | public JSBackend (IBackend handler) 18 | { 19 | Handler = handler; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Bootsharp.Inject 8 | Bootsharp.Inject 9 | Dependency injection extensions for Bootsharp. 10 | true 11 | CS1591 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Inject/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Bootsharp.Inject; 4 | 5 | /// 6 | /// Extension methods for dependency injection. 7 | /// 8 | public static class Extensions 9 | { 10 | /// 11 | /// Registers JavaScript bindings generated by Bootsharp. 12 | /// 13 | public static IServiceCollection AddBootsharp (this IServiceCollection services) 14 | { 15 | foreach (var (impl, binding) in Interfaces.Exports) 16 | services.AddSingleton(impl, provider => { 17 | var handler = provider.GetService(binding.Interface); 18 | if (handler is null) throw new Error($"Failed to run Bootsharp: '{binding.Interface}' dependency is not registered."); 19 | return binding.Factory(provider.GetRequiredService(binding.Interface)); 20 | }); 21 | foreach (var (api, binding) in Interfaces.Imports) 22 | services.AddSingleton(api, binding.Instance); 23 | return services; 24 | } 25 | 26 | /// 27 | /// Initializes exported JavaScript bindings generated by Bootsharp. 28 | /// 29 | public static IServiceProvider RunBootsharp (this IServiceProvider provider) 30 | { 31 | foreach (var (impl, _) in Interfaces.Exports) 32 | provider.GetRequiredService(impl); 33 | return provider; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish.Test; 2 | 3 | public class EmitTest : TaskTest 4 | { 5 | protected BootsharpEmit Task { get; } 6 | protected string GeneratedInterfaces => ReadProjectFile(interfacesPath); 7 | protected string GeneratedDependencies => ReadProjectFile(dependenciesPath); 8 | protected string GeneratedSerializer => ReadProjectFile(serializerPath); 9 | protected string GeneratedInterop => ReadProjectFile(interopPath); 10 | 11 | private string interfacesPath => $"{Project.Root}/Interfaces.g.cs"; 12 | private string dependenciesPath => $"{Project.Root}/Dependencies.g.cs"; 13 | private string serializerPath => $"{Project.Root}/Serializer.g.cs"; 14 | private string interopPath => $"{Project.Root}/Interop.g.cs"; 15 | 16 | public EmitTest () 17 | { 18 | Task = CreateTask(); 19 | } 20 | 21 | public override void Execute () 22 | { 23 | if (LastAddedAssemblyName is not null) 24 | Task.EntryAssemblyName = LastAddedAssemblyName; 25 | Task.Execute(); 26 | } 27 | 28 | private BootsharpEmit CreateTask () => new() { 29 | InspectedDirectory = Project.Root, 30 | EntryAssemblyName = "System.Runtime.dll", 31 | InterfacesFilePath = interfacesPath, 32 | DependenciesFilePath = dependenciesPath, 33 | SerializerFilePath = serializerPath, 34 | InteropFilePath = interopPath, 35 | BuildEngine = Engine 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Mock/MockAssembly.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish.Test; 2 | 3 | public record MockAssembly (string Name, MockSource[] Sources); 4 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | 4 | namespace Bootsharp.Publish.Test; 5 | 6 | public class MockCompiler 7 | { 8 | private static readonly string[] defaultUsings = [ 9 | "System", 10 | "System.Collections.Generic", 11 | "System.Threading.Tasks", 12 | "Bootsharp" 13 | ]; 14 | 15 | public void Compile (IEnumerable sources, string assemblyPath) 16 | { 17 | var source = 18 | $""" 19 | #nullable enable 20 | {string.Join('\n', defaultUsings.Select(u => $"using {u};"))} 21 | {string.Join('\n', sources.Select(BuildSource))} 22 | """; 23 | var compilation = CreateCompilation(assemblyPath, source); 24 | var result = compilation.Emit(assemblyPath); 25 | if (result.Success) return; 26 | var error = $"Invalid test source code: {result.Diagnostics.First().GetMessage()}"; 27 | Assert.Fail(string.Join('\n', [error, "---", source, "---"])); 28 | } 29 | 30 | private static string BuildSource (MockSource source) 31 | { 32 | var text = source.WrapInClass 33 | ? $"public partial class Class {{ {source.Code} }}" 34 | : source.Code; 35 | return source.Namespace is null ? text 36 | : $"namespace {source.Namespace} {{ {text} }}"; 37 | } 38 | 39 | private static CSharpCompilation CreateCompilation (string assemblyPath, string text) 40 | { 41 | var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); 42 | var tree = CSharpSyntaxTree.ParseText(text); 43 | var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); 44 | var refs = GatherReferences(Path.GetDirectoryName(assemblyPath)); 45 | return CSharpCompilation.Create(assemblyName, [tree], refs, options); 46 | } 47 | 48 | private static PortableExecutableReference[] GatherReferences (string directory) 49 | { 50 | var paths = Directory.GetFiles(directory, "*.dll"); 51 | return paths.Select(p => MetadataReference.CreateFromFile(p)).ToArray(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Mock/MockProject.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Bootsharp.Publish.Test; 4 | 5 | public sealed class MockProject : IDisposable 6 | { 7 | public string Root { get; } 8 | 9 | private readonly MockCompiler compiler = new(); 10 | 11 | public MockProject () 12 | { 13 | Root = CreateUniqueRootDirectory(); 14 | CreateBuildResources(); 15 | } 16 | 17 | public void Dispose () => Directory.Delete(Root, true); 18 | 19 | public void AddAssembly (MockAssembly assembly) 20 | { 21 | var assemblyPath = Path.Combine(Root, assembly.Name); 22 | compiler.Compile(assembly.Sources, assemblyPath); 23 | } 24 | 25 | public void WriteFile (string name, ReadOnlySpan content) 26 | { 27 | var filePath = Path.Combine(Root, name); 28 | File.WriteAllBytes(filePath, content.ToArray()); 29 | } 30 | 31 | public void WriteFile (string name, string content) 32 | { 33 | var filePath = Path.Combine(Root, name); 34 | File.WriteAllText(filePath, content); 35 | } 36 | 37 | private static string CreateUniqueRootDirectory () 38 | { 39 | var testAssembly = System.Reflection.Assembly.GetExecutingAssembly().Location; 40 | var assemblyDir = Path.Combine(Path.GetDirectoryName(testAssembly)); 41 | var dir = $"bootsharp-temp-{Guid.NewGuid():N}"; 42 | return Directory.CreateDirectory(Path.Combine(assemblyDir, dir)).FullName; 43 | } 44 | 45 | private void CreateBuildResources () 46 | { 47 | foreach (var path in GetReferencePaths()) 48 | File.Copy(path, Path.Combine(Root, Path.GetFileName(path)), true); 49 | } 50 | 51 | private static string[] GetReferencePaths () 52 | { 53 | var coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location); 54 | return [ 55 | MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")).FilePath, 56 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location).FilePath, 57 | MetadataReference.CreateFromFile(typeof(JSExportAttribute).Assembly.Location).FilePath 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Mock/MockSource.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish.Test; 2 | 3 | public record MockSource (string Namespace, string Code, bool WrapInClass); 4 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Mock/MockTest.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Sdk; 2 | 3 | namespace Bootsharp.Publish.Test; 4 | 5 | public class MockTest 6 | { 7 | [Fact] 8 | public void WhenCompileFailsIncludesSourceAndError () 9 | { 10 | using var project = new MockProject(); 11 | var asm = new MockAssembly("asm.dll", [new(null, "foo", false)]); 12 | Assert.Contains("Invalid test source code", Assert.Throws(() => project.AddAssembly(asm)).Message); 13 | Assert.Contains("foo", Assert.Throws(() => project.AddAssembly(asm)).Message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish.Test; 2 | 3 | public class PackTest : TaskTest 4 | { 5 | protected BootsharpPack Task { get; } 6 | protected byte[] MockWasmBinary { get; } = "MockWasmContent"u8.ToArray(); 7 | protected string MockDotNetContent { get; } = "MockDotNetContent"; 8 | protected string MockRuntimeContent { get; } = "MockRuntimeContent"; 9 | protected string MockNativeContent { get; } = "MockNativeContent"; 10 | protected string GeneratedBindings => ReadProjectFile("bindings.g.js"); 11 | protected string GeneratedDeclarations => ReadProjectFile("bindings.g.d.ts"); 12 | protected string GeneratedResources => ReadProjectFile("resources.g.js"); 13 | protected string GeneratedDotNetModule => ReadProjectFile("dotnet.g.js"); 14 | protected string GeneratedRuntimeModule => ReadProjectFile("dotnet.runtime.g.js"); 15 | protected string GeneratedNativeModule => ReadProjectFile("dotnet.native.g.js"); 16 | 17 | public PackTest () 18 | { 19 | Task = CreateTask(); 20 | Project.WriteFile("dotnet.js", MockDotNetContent); 21 | Project.WriteFile("dotnet.runtime.js", MockRuntimeContent); 22 | Project.WriteFile("dotnet.native.js", MockNativeContent); 23 | Project.WriteFile("dotnet.runtime.g.js", "MockRuntimeGeneratedContent"); 24 | Project.WriteFile("dotnet.native.g.js", "MockNativeGeneratedContent"); 25 | Project.WriteFile("dotnet.native.wasm", MockWasmBinary); 26 | } 27 | 28 | public override void Execute () 29 | { 30 | if (LastAddedAssemblyName is not null) 31 | Task.EntryAssemblyName = LastAddedAssemblyName; 32 | Task.Execute(); 33 | } 34 | 35 | protected override void AddAssembly (string assemblyName, params MockSource[] sources) 36 | { 37 | base.AddAssembly(assemblyName, sources); 38 | Project.WriteFile(assemblyName[..^3] + "wasm", ""); 39 | } 40 | 41 | private BootsharpPack CreateTask () => new() { 42 | BuildDirectory = Project.Root, 43 | InspectedDirectory = Project.Root, 44 | EntryAssemblyName = "System.Runtime.dll", 45 | BuildEngine = Engine, 46 | TrimmingEnabled = false, 47 | EmbedBinaries = false, 48 | Threading = false, 49 | LLVM = false 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish.Test; 2 | 3 | public class ResourceTest : PackTest 4 | { 5 | protected override string TestedContent => GeneratedResources; 6 | 7 | [Fact] 8 | public void EntryAssemblyNameIsWritten () 9 | { 10 | AddAssembly("Foo.dll"); 11 | Execute(); 12 | Contains("entryAssemblyName: \"Foo.dll\""); 13 | } 14 | 15 | [Fact] 16 | public void BinariesEmbeddedWhenEnabled () 17 | { 18 | AddAssembly("Foo.dll"); 19 | Task.EmbedBinaries = true; 20 | Execute(); 21 | Contains($$"""wasm: { name: "dotnet.native.wasm", content: "{{Convert.ToBase64String(MockWasmBinary)}}" },"""); 22 | Contains("{ name: \"Foo.wasm\", content: \""); 23 | } 24 | 25 | [Fact] 26 | public void BinariesNotEmbeddedWhenDisabled () 27 | { 28 | AddAssembly("Foo.dll"); 29 | Task.EmbedBinaries = false; 30 | Execute(); 31 | Contains("""wasm: { name: "dotnet.native.wasm", content: undefined },"""); 32 | Contains("""{ name: "Foo.wasm", content: undefined"""); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/Packer.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish.Test/TaskTest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.Build.Utilities.ProjectCreation; 3 | 4 | namespace Bootsharp.Publish.Test; 5 | 6 | public abstract class TaskTest : IDisposable 7 | { 8 | protected MockProject Project { get; } = new(); 9 | protected BuildEngine Engine { get; } = BuildEngine.Create(); 10 | protected string LastAddedAssemblyName { get; private set; } 11 | protected virtual string TestedContent { get; } = ""; 12 | 13 | public abstract void Execute (); 14 | 15 | public void Dispose () 16 | { 17 | Project.Dispose(); 18 | GC.SuppressFinalize(this); 19 | } 20 | 21 | protected virtual void AddAssembly (string assemblyName, params MockSource[] sources) 22 | { 23 | LastAddedAssemblyName = assemblyName; 24 | Project.AddAssembly(new(assemblyName, sources)); 25 | } 26 | 27 | protected void AddAssembly (params MockSource[] sources) 28 | { 29 | AddAssembly($"MockAssembly{Guid.NewGuid():N}.dll", sources); 30 | } 31 | 32 | protected MockSource WithClass (string @namespace, string body) 33 | { 34 | return new(@namespace, body, true); 35 | } 36 | 37 | protected MockSource WithClass (string body) 38 | { 39 | return WithClass(null, body); 40 | } 41 | 42 | protected MockSource With (string @namespace, string code) 43 | { 44 | return new(@namespace, code, false); 45 | } 46 | 47 | protected MockSource With (string code) 48 | { 49 | return With(null, code); 50 | } 51 | 52 | protected void Contains (string content) 53 | { 54 | Assert.Contains(content, TestedContent); 55 | } 56 | 57 | protected void DoesNotContain (string content) 58 | { 59 | Assert.DoesNotContain(content, TestedContent, StringComparison.OrdinalIgnoreCase); 60 | } 61 | 62 | protected MatchCollection Matches (string pattern) 63 | { 64 | Assert.Matches(pattern, TestedContent); 65 | return Regex.Matches(TestedContent, pattern); 66 | } 67 | 68 | protected string ReadProjectFile (string fileName) 69 | { 70 | var filePath = Path.Combine(Project.Root, fileName); 71 | return File.Exists(filePath) ? File.ReadAllText(filePath) : null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs: -------------------------------------------------------------------------------- 1 | global using static Bootsharp.Publish.GlobalInspection; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Bootsharp.Publish; 7 | 8 | internal static class GlobalInspection 9 | { 10 | public static MetadataLoadContext CreateLoadContext (string directory) 11 | { 12 | var assemblyPaths = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll").ToList(); 13 | foreach (var path in Directory.GetFiles(directory, "*.dll")) 14 | if (assemblyPaths.All(p => Path.GetFileName(p) != Path.GetFileName(path))) 15 | assemblyPaths.Add(path); 16 | var resolver = new PathAssemblyResolver(assemblyPaths); 17 | return new MetadataLoadContext(resolver); 18 | } 19 | 20 | public static bool ShouldIgnoreAssembly (string filePath) 21 | { 22 | var assemblyName = Path.GetFileName(filePath); 23 | return assemblyName.StartsWith("System.") || 24 | assemblyName.StartsWith("Microsoft.") || 25 | assemblyName.StartsWith("netstandard") || 26 | assemblyName.StartsWith("mscorlib"); 27 | } 28 | 29 | public static string WithPrefs (IReadOnlyCollection prefs, string input, string @default) 30 | { 31 | foreach (var pref in prefs) 32 | if (Regex.IsMatch(input, pref.Pattern)) 33 | return Regex.Replace(input, pref.Pattern, pref.Replacement); 34 | return @default; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs: -------------------------------------------------------------------------------- 1 | global using static Bootsharp.Publish.GlobalText; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | internal static class GlobalText 6 | { 7 | public static string JoinLines (IEnumerable values, int indent = 1, string separator = "\n") 8 | { 9 | if (indent > 0) separator += new string(' ', indent * 4); 10 | return string.Join(separator, values.Where(v => v is not null)); 11 | } 12 | 13 | public static string JoinLines (params string?[] values) => JoinLines(values, 1); 14 | public static string JoinLines (int indent, params string?[] values) => JoinLines(values, indent); 15 | 16 | public static string ToFirstLower (string value) 17 | { 18 | if (value.Length == 1) return value.ToLowerInvariant(); 19 | return char.ToLowerInvariant(value[0]) + value[1..]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Meta/ArgumentMeta.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | /// Interop method argument. 5 | /// 6 | internal sealed record ArgumentMeta 7 | { 8 | /// 9 | /// C# name of the argument, as specified in source code. 10 | /// 11 | public required string Name { get; init; } 12 | /// 13 | /// JavaScript name of the argument, to be specified in source code. 14 | /// 15 | public required string JSName { get; init; } 16 | /// 17 | /// Metadata of the argument's value. 18 | /// 19 | public required ValueMeta Value { get; init; } 20 | 21 | public override string ToString () => $"{Name}: {Value.JSTypeSyntax}"; 22 | } 23 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Meta/InterfaceKind.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | /// The type of API interop interface represents. 5 | /// 6 | internal enum InterfaceKind 7 | { 8 | /// 9 | /// The interface represents C# API consumed in JavaScript. 10 | /// 11 | Export, 12 | /// 13 | /// The interface represents JavaScript API consumed in C#. 14 | /// 15 | Import 16 | } 17 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | /// Interface supplied by user under either 5 | /// or representing static interop API, or in 6 | /// an interop method, representing instanced interop API. 7 | /// 8 | internal sealed record InterfaceMeta 9 | { 10 | /// 11 | /// Whether the interface represents C# API consumed in 12 | /// JavaScript (export) or vice-versa (import). 13 | /// 14 | public required InterfaceKind Kind { get; init; } 15 | /// 16 | /// C# type of the interface. 17 | /// 18 | public required Type Type { get; init; } 19 | /// 20 | /// C# syntax of the interface type, as specified in source code. 21 | /// 22 | public required string TypeSyntax { get; init; } 23 | /// 24 | /// Namespace of the generated interop class implementation. 25 | /// 26 | public required string Namespace { get; init; } 27 | /// 28 | /// Name of the generated interop class implementation. 29 | /// 30 | public required string Name { get; init; } 31 | /// 32 | /// Full type name of the generated interop class implementation. 33 | /// 34 | public string FullName => $"{Namespace}.{Name}"; 35 | /// 36 | /// Methods declared on the interface, representing the interop API. 37 | /// 38 | public required IReadOnlyCollection Methods { get; init; } 39 | } 40 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Meta/MethodKind.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | /// Type of interop method. 5 | /// 6 | internal enum MethodKind 7 | { 8 | /// 9 | /// The method is implemented in C# and invoked from JavaScript; 10 | /// implementation has . 11 | /// 12 | Invokable, 13 | /// 14 | /// The method is implemented in JavaScript and invoked from C#; 15 | /// implementation has . 16 | /// 17 | Function, 18 | /// 19 | /// The method is invoked from C# to notify subscribers in JavaScript; 20 | /// implementation has . 21 | /// 22 | Event 23 | } 24 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Meta/MethodMeta.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | /// Interop method. 5 | /// 6 | internal sealed record MethodMeta 7 | { 8 | /// 9 | /// Type of interop the method is implementing. 10 | /// 11 | public required MethodKind Kind { get; init; } 12 | /// 13 | /// C# assembly name (DLL file name, w/o the extension), under which the method is declared. 14 | /// 15 | public required string Assembly { get; init; } 16 | /// 17 | /// Full name of the C# type (including namespace), under which the method is declared. 18 | /// 19 | public required string Space { get; init; } 20 | /// 21 | /// JavaScript object name(s) (joined with dot when nested) under which the associated interop 22 | /// function will be declared; resolved from with user-defined converters. 23 | /// 24 | public required string JSSpace { get; init; } 25 | /// 26 | /// C# name of the method, as specified in source code. 27 | /// 28 | public required string Name { get; init; } 29 | /// 30 | /// JavaScript name of the method (function), as will be specified in source code. 31 | /// 32 | public required string JSName { get; init; } 33 | /// 34 | /// When the method's class is a generated implementation of an interop interface, contains 35 | /// name of the associated interface method. The name may differ from , 36 | /// which would be the name of the method on the generated interface implementation and is 37 | /// subject to and . 38 | /// 39 | public string? InterfaceName { get; init; } 40 | /// 41 | /// Arguments of the method, in declaration order. 42 | /// 43 | public required IReadOnlyList Arguments { get; init; } 44 | /// 45 | /// Metadata of the value returned by the method. 46 | /// 47 | public required ValueMeta ReturnValue { get; init; } 48 | 49 | public override string ToString () 50 | { 51 | var args = string.Join(", ", Arguments.Select(a => a.ToString())); 52 | return $"[{Kind}] {Assembly}.{Space}.{Name} ({args}) => {ReturnValue}"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | /// 6 | /// Interop method's argument or returned value. 7 | /// 8 | internal sealed record ValueMeta 9 | { 10 | /// 11 | /// C# type of the value. 12 | /// 13 | public required Type Type { get; init; } 14 | /// 15 | /// C# syntax of the value type, as specified in source code. 16 | /// 17 | public required string TypeSyntax { get; init; } 18 | /// 19 | /// TypeScript syntax of the value type, to be specified in source code. 20 | /// 21 | public required string JSTypeSyntax { get; init; } 22 | /// 23 | /// Serialization info handle for the type. 24 | /// 25 | public required string TypeInfo { get; init; } 26 | /// 27 | /// Whether the value is optional/nullable. 28 | /// 29 | public required bool Nullable { get; init; } 30 | /// 31 | /// Whether the value type is of an async nature (eg, task or promise). 32 | /// 33 | public required bool Async { get; init; } 34 | /// 35 | /// Whether the value is void (when method return value). 36 | /// 37 | public required bool Void { get; init; } 38 | /// 39 | /// Whether the value has to be marshalled to/from JSON for interop. 40 | /// 41 | public required bool Serialized { get; init; } 42 | /// 43 | /// Whether the value is an interop instance. 44 | /// 45 | [MemberNotNullWhen(true, nameof(InstanceType))] 46 | public required bool Instance { get; init; } 47 | /// 48 | /// When contains type of the associated interop interface instance. 49 | /// 50 | public required Type? InstanceType { get; init; } 51 | } 52 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Preferences/Preference.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | internal sealed record Preference (string Pattern, string Replacement); 4 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | internal sealed record Preferences 5 | { 6 | /// 7 | public IReadOnlyList Space { get; init; } = []; 8 | /// 9 | public IReadOnlyList Type { get; init; } = []; 10 | /// 11 | public IReadOnlyList Event { get; init; } = []; 12 | /// 13 | public IReadOnlyList Function { get; init; } = []; 14 | } 15 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | internal sealed class PreferencesResolver (string entryAssemblyName) 6 | { 7 | public Preferences Resolve (string outDir) 8 | { 9 | using var ctx = CreateLoadContext(outDir); 10 | var assemblyPath = Path.Combine(outDir, entryAssemblyName); 11 | var assembly = ctx.LoadFromAssemblyPath(assemblyPath); 12 | var attribute = FindPreferencesAttribute(assembly); 13 | return CreatePreferences(attribute); 14 | } 15 | 16 | private CustomAttributeData? FindPreferencesAttribute (Assembly assembly) 17 | { 18 | foreach (var attr in assembly.CustomAttributes) 19 | if (attr.AttributeType.FullName == typeof(JSPreferencesAttribute).FullName) 20 | return attr; 21 | return null; 22 | } 23 | 24 | private Preferences CreatePreferences (CustomAttributeData? attr) => new() { 25 | Space = CreatePreferences(nameof(JSPreferencesAttribute.Space), attr) ?? [], 26 | Type = CreatePreferences(nameof(JSPreferencesAttribute.Type), attr) ?? [], 27 | Event = CreatePreferences(nameof(JSPreferencesAttribute.Event), attr) ?? [new(@"^Notify(\S+)", "On$1")], 28 | Function = CreatePreferences(nameof(JSPreferencesAttribute.Function), attr) ?? [] 29 | }; 30 | 31 | private Preference[]? CreatePreferences (string name, CustomAttributeData? attr) 32 | { 33 | if (attr is null || !attr.NamedArguments.Any(a => a.MemberName == name)) return null; 34 | var value = CreateValue(attr.NamedArguments.First(a => a.MemberName == name).TypedValue); 35 | var prefs = new Preference[value.Length / 2]; 36 | for (int i = 0; i < prefs.Length; i++) 37 | prefs[i] = new(value[i * 2], value[(i * 2) + 1]); 38 | return prefs; 39 | } 40 | 41 | private string[] CreateValue (CustomAttributeTypedArgument arg) 42 | { 43 | var items = ((IEnumerable)arg.Value!).ToArray(); 44 | var value = new string[items.Length]; 45 | for (int i = 0; i < items.Length; i++) 46 | value[i] = (string)items[i].Value!; 47 | return value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Framework; 2 | using Microsoft.Build.Utilities; 3 | 4 | namespace Bootsharp.Publish; 5 | 6 | internal sealed class InspectionReporter (TaskLoggingHelper logger) 7 | { 8 | public void Report (SolutionInspection inspection) 9 | { 10 | logger.LogMessage(MessageImportance.Normal, "Bootsharp assembly inspection result:"); 11 | logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered assemblies:", 12 | JoinLines(GetDiscoveredAssemblies(inspection)))); 13 | logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered interop methods:", 14 | JoinLines(GetDiscoveredMethods(inspection)))); 15 | foreach (var warning in inspection.Warnings) 16 | logger.LogWarning(warning); 17 | } 18 | 19 | private HashSet GetDiscoveredAssemblies (SolutionInspection inspection) 20 | { 21 | return inspection.StaticMethods 22 | .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) 23 | .Select(m => m.Assembly) 24 | .ToHashSet(); 25 | } 26 | 27 | private HashSet GetDiscoveredMethods (SolutionInspection inspection) 28 | { 29 | return inspection.StaticMethods 30 | .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) 31 | .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)) 32 | .Select(m => m.ToString()) 33 | .ToHashSet(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | internal sealed class InterfaceInspector (Preferences prefs, TypeConverter converter, string entryAssemblyName) 6 | { 7 | private readonly MethodInspector methodInspector = new(prefs, converter); 8 | 9 | public InterfaceMeta Inspect (Type interfaceType, InterfaceKind kind) 10 | { 11 | var impl = BuildInteropInterfaceImplementationName(interfaceType, kind); 12 | return new InterfaceMeta { 13 | Kind = kind, 14 | Type = interfaceType, 15 | TypeSyntax = BuildSyntax(interfaceType), 16 | Namespace = impl.space, 17 | Name = impl.name, 18 | Methods = interfaceType.GetMethods() 19 | .Where(m => m.IsAbstract) 20 | .Select(m => CreateMethod(m, kind, impl.full)).ToArray() 21 | }; 22 | } 23 | 24 | private MethodMeta CreateMethod (MethodInfo info, InterfaceKind kind, string space) 25 | { 26 | var name = WithPrefs(prefs.Event, info.Name, info.Name); 27 | return methodInspector.Inspect(info, ResolveMethodKind(kind, info, name)) with { 28 | Assembly = entryAssemblyName, 29 | Space = space, 30 | Name = name, 31 | JSName = ToFirstLower(name), 32 | InterfaceName = info.Name 33 | }; 34 | } 35 | 36 | private MethodKind ResolveMethodKind (InterfaceKind iKind, MethodInfo info, string implMethodName) 37 | { 38 | if (iKind == InterfaceKind.Export) return MethodKind.Invokable; 39 | // TODO: This assumes event methods are always renamed via prefs, which may not be the case. 40 | if (implMethodName != info.Name) return MethodKind.Event; 41 | return MethodKind.Function; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | internal sealed class MethodInspector (Preferences prefs, TypeConverter converter) 6 | { 7 | private MethodInfo method = null!; 8 | private MethodKind kind; 9 | 10 | public MethodMeta Inspect (MethodInfo method, MethodKind kind) 11 | { 12 | this.method = method; 13 | this.kind = kind; 14 | return CreateMethod(); 15 | } 16 | 17 | private MethodMeta CreateMethod () => new() { 18 | Kind = kind, 19 | Assembly = method.DeclaringType!.Assembly.GetName().Name!, 20 | Space = method.DeclaringType.FullName!, 21 | Name = method.Name, 22 | Arguments = method.GetParameters().Select(CreateArgument).ToArray(), 23 | ReturnValue = CreateValue(method.ReturnParameter, true), 24 | JSSpace = BuildMethodSpace(), 25 | JSName = WithPrefs(prefs.Function, method.Name, ToFirstLower(method.Name)) 26 | }; 27 | 28 | private ArgumentMeta CreateArgument (ParameterInfo param) => new() { 29 | Name = param.Name!, 30 | JSName = param.Name == "function" ? "fn" : param.Name!, 31 | Value = CreateValue(param, false) 32 | }; 33 | 34 | private ValueMeta CreateValue (ParameterInfo param, bool @return) => new() { 35 | Type = param.ParameterType, 36 | TypeSyntax = BuildSyntax(param.ParameterType, param), 37 | JSTypeSyntax = converter.ToTypeScript(param.ParameterType, GetNullability(param)), 38 | TypeInfo = BuildTypeInfo(param.ParameterType), 39 | Nullable = @return ? IsNullable(method) : IsNullable(param), 40 | Async = @return && IsTaskLike(param.ParameterType), 41 | Void = @return && IsVoid(param.ParameterType), 42 | Serialized = ShouldSerialize(param.ParameterType), 43 | Instance = IsInstancedInteropInterface(param.ParameterType, out var instanceType), 44 | InstanceType = instanceType 45 | }; 46 | 47 | private string BuildMethodSpace () 48 | { 49 | var space = method.DeclaringType!.Namespace ?? ""; 50 | var name = BuildJSSpaceName(method.DeclaringType); 51 | if (method.DeclaringType.IsInterface) name = name[1..]; 52 | var fullname = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; 53 | return WithPrefs(prefs.Space, fullname, fullname); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | /// 6 | /// Metadata about the built C# solution required to generate interop 7 | /// code and other Bootsharp-specific resources. 8 | /// 9 | /// 10 | /// Context in which the solution's assemblies were loaded and inspected. 11 | /// Shouldn't be disposed to keep C# reflection APIs usable on the inspected types. 12 | /// Dispose to remove file lock on the inspected assemblies. 13 | /// 14 | internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable 15 | { 16 | /// 17 | /// Interop interfaces specified under or 18 | /// for which static bindings have to be emitted. 19 | /// 20 | public required IReadOnlyCollection StaticInterfaces { get; init; } 21 | /// 22 | /// Interop interfaces found in interop method arguments or return values. Such 23 | /// interfaces are considered instanced interop APIs, ie stateful objects with 24 | /// interop methods/functions. Both methods of and 25 | /// can be sources of the instanced interfaces. 26 | /// 27 | public required IReadOnlyCollection InstancedInterfaces { get; init; } 28 | /// 29 | /// Static interop methods, ie methods with 30 | /// and similar interop attributes found on user-defined static classes. 31 | /// 32 | public required IReadOnlyCollection StaticMethods { get; init; } 33 | /// 34 | /// Types referenced on the interop methods (both static and on interfaces), 35 | /// as well as types they depend on (ie, implemented interfaces). 36 | /// Basically, all the types that have to pass interop boundary. 37 | /// 38 | public required IReadOnlyCollection Crawled { get; init; } 39 | /// 40 | /// Warnings logged while inspecting solution. 41 | /// 42 | public required IReadOnlyCollection Warnings { get; init; } 43 | 44 | public void Dispose () => ctx.Dispose(); 45 | } 46 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | internal sealed class TypeCrawler 4 | { 5 | public IReadOnlyCollection Crawled => crawled; 6 | 7 | private readonly HashSet crawled = []; 8 | 9 | public void Crawl (Type type) 10 | { 11 | if (!ShouldCrawl(type)) return; 12 | var underlyingType = GetUnderlyingType(type); 13 | if (!crawled.Add(underlyingType)) return; 14 | CrawlProperties(underlyingType); 15 | CrawlBaseType(underlyingType); 16 | crawled.Add(type); 17 | } 18 | 19 | private bool ShouldCrawl (Type type) 20 | { 21 | type = GetUnderlyingType(type); 22 | return (Type.GetTypeCode(type) == TypeCode.Object || type.IsEnum) && 23 | !ShouldIgnoreAssembly(type.Assembly.FullName!); 24 | } 25 | 26 | private void CrawlProperties (Type type) 27 | { 28 | var propertyTypesToAdd = type.GetProperties() 29 | .Select(m => m.PropertyType) 30 | .Where(ShouldCrawl); 31 | foreach (var propertyType in propertyTypesToAdd) 32 | Crawl(propertyType); 33 | } 34 | 35 | private void CrawlBaseType (Type type) 36 | { 37 | if (type.BaseType != null && ShouldCrawl(type.BaseType)) 38 | Crawl(type.BaseType); 39 | } 40 | 41 | private Type GetUnderlyingType (Type type) 42 | { 43 | if (IsNullable(type)) return GetNullableUnderlyingType(type); 44 | if (IsList(type) || IsCollection(type)) return GetUnderlyingType(GetListElementType(type)); 45 | return type; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; 3 | 4 | namespace Bootsharp.Publish; 5 | 6 | /// 7 | /// Generates hints for .NET to not trim specified dynamic dependencies, ie 8 | /// members that are not explicitly accessed in the user source code. 9 | /// 10 | internal sealed class DependencyGenerator (string entryAssembly) 11 | { 12 | private readonly HashSet added = []; 13 | 14 | public string Generate (SolutionInspection inspection) 15 | { 16 | AddGeneratedCommon(); 17 | AddGeneratedInteropClasses(inspection); 18 | AddClassesWithInteropMethods(inspection); 19 | return 20 | $$""" 21 | using System.Diagnostics.CodeAnalysis; 22 | 23 | namespace Bootsharp.Generated; 24 | 25 | public static class Dependencies 26 | { 27 | [System.Runtime.CompilerServices.ModuleInitializer] 28 | {{JoinLines(added)}} 29 | internal static void RegisterDynamicDependencies () { } 30 | } 31 | """; 32 | } 33 | 34 | private void AddGeneratedCommon () 35 | { 36 | Add(All, "Bootsharp.Generated.Dependencies", entryAssembly); 37 | Add(All, "Bootsharp.Generated.Interop", entryAssembly); 38 | } 39 | 40 | private void AddGeneratedInteropClasses (SolutionInspection inspection) 41 | { 42 | foreach (var inter in inspection.StaticInterfaces) 43 | Add(All, inter.FullName, entryAssembly); 44 | foreach (var inter in inspection.InstancedInterfaces) 45 | if (inter.Kind == InterfaceKind.Import) 46 | Add(All, inter.FullName, entryAssembly); 47 | } 48 | 49 | private void AddClassesWithInteropMethods (SolutionInspection inspection) 50 | { 51 | foreach (var method in inspection.StaticMethods) 52 | Add(All, method.Space, method.Assembly); 53 | } 54 | 55 | private void Add (DynamicallyAccessedMemberTypes types, string name, string assembly) 56 | { 57 | var asm = assembly.EndsWith(".dll", StringComparison.Ordinal) ? assembly[..^4] : assembly; 58 | added.Add($"""[DynamicDependency(DynamicallyAccessedMemberTypes.{types}, "{name}", "{asm}")]"""); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | /// 4 | /// Generates hints for all the types used in interop to be picked by 5 | /// .NET's JSON serializer source generator. Required for the serializer to 6 | /// work without using reflection (which is required to support trimming). 7 | /// 8 | internal sealed class SerializerGenerator 9 | { 10 | private readonly HashSet attributes = []; 11 | 12 | public string Generate (SolutionInspection inspection) 13 | { 14 | CollectTopLevel(inspection); 15 | CollectCrawled(inspection); 16 | if (attributes.Count == 0) return ""; 17 | return 18 | $""" 19 | using System.Text.Json.Serialization; 20 | 21 | namespace Bootsharp.Generated; 22 | 23 | {JoinLines(attributes, 0)} 24 | [JsonSourceGenerationOptions( 25 | PropertyNameCaseInsensitive = true, 26 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, 27 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 28 | )] 29 | internal partial class SerializerContext : JsonSerializerContext; 30 | """; 31 | } 32 | 33 | private void CollectTopLevel (SolutionInspection inspection) 34 | { 35 | var metas = inspection.StaticMethods 36 | .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) 37 | .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)); 38 | foreach (var meta in metas) 39 | CollectFromMethod(meta); 40 | } 41 | 42 | private void CollectFromMethod (MethodMeta method) 43 | { 44 | if (method.ReturnValue.Serialized) 45 | CollectFromValue(method.ReturnValue); 46 | foreach (var arg in method.Arguments) 47 | if (arg.Value.Serialized) 48 | CollectFromValue(arg.Value); 49 | } 50 | 51 | private void CollectFromValue (ValueMeta meta) 52 | { 53 | attributes.Add(BuildAttribute(meta.Type)); 54 | } 55 | 56 | private void CollectCrawled (SolutionInspection inspection) 57 | { 58 | foreach (var type in inspection.Crawled) 59 | if (ShouldSerialize(type)) 60 | attributes.Add(BuildAttribute(type)); 61 | } 62 | 63 | private static string BuildAttribute (Type type) 64 | { 65 | var syntax = IsTaskWithResult(type, out var result) ? BuildSyntax(result) : BuildSyntax(type); 66 | var info = BuildTypeInfo(type); 67 | return $"[JsonSerializable(typeof({syntax}), TypeInfoPropertyName = \"{info}\")]"; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | internal sealed class BindingClassGenerator 4 | { 5 | public string Generate (IReadOnlyCollection instanced) 6 | { 7 | var exported = instanced.Where(i => i.Kind == InterfaceKind.Export); 8 | return JoinLines(exported.Select(BuildClass), 0) + '\n'; 9 | } 10 | 11 | private string BuildClass (InterfaceMeta inter) => 12 | $$""" 13 | class {{BuildJSInteropInstanceClassName(inter)}} { 14 | constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } 15 | {{JoinLines(inter.Methods.Select(BuildFunction))}} 16 | } 17 | """; 18 | 19 | private string BuildFunction (MethodMeta inv) 20 | { 21 | var sigArgs = string.Join(", ", inv.Arguments.Select(a => a.Name)); 22 | var args = "this._id" + (sigArgs.Length > 0 ? $", {sigArgs}" : ""); 23 | var @return = inv.ReturnValue.Void ? "" : "return "; 24 | return $"{inv.JSName}({sigArgs}) {{ {@return}{inv.JSSpace}.{inv.JSName}({args}); }}"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | internal sealed class DeclarationGenerator (Preferences prefs) 4 | { 5 | private readonly MethodDeclarationGenerator methodsGenerator = new(); 6 | private readonly TypeDeclarationGenerator typesGenerator = new(prefs); 7 | 8 | public string Generate (SolutionInspection inspection) => JoinLines(0, 9 | """import type { Event } from "./event";""", 10 | typesGenerator.Generate(inspection), 11 | methodsGenerator.Generate(inspection) 12 | ) + "\n"; 13 | } 14 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Bootsharp.Publish; 4 | 5 | internal sealed class InternalPatcher (string dotnet, string runtime, string native) 6 | { 7 | private const string url = 8 | """ 9 | ((typeof window === "object" && "Deno" in window && Deno.build.os === "windows") || (typeof process === "object" && process.platform === "win32")) ? "file://dotnet.native.wasm" : "file:///dotnet.native.wasm" 10 | """; 11 | 12 | public void Patch () 13 | { 14 | // Remove unnecessary environment-specific calls in .NET's internals, 15 | // that are offending bundlers and breaking usage in restricted environments, 16 | // such as VS Code web extensions. (https://github.com/elringus/bootsharp/issues/139) 17 | 18 | File.WriteAllText(dotnet, File.ReadAllText(dotnet, Encoding.UTF8) 19 | .Replace("import.meta.url", url) 20 | .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); 21 | 22 | File.WriteAllText(runtime, File.ReadAllText(runtime, Encoding.UTF8) 23 | .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); 24 | 25 | File.WriteAllText(native, File.ReadAllText(native, Encoding.UTF8) 26 | .Replace("var _scriptDir = import.meta.url", "var _scriptDir = \"file:/\"") 27 | .Replace("require('url').fileURLToPath(new URL('./', import.meta.url))", "\"./\"") 28 | .Replace("require(\"url\").fileURLToPath(new URL(\"./\",import.meta.url))", "\"./\"") // when aggressive trimming enabled 29 | .Replace("new URL('dotnet.native.wasm', import.meta.url).href", "\"file:/\"") 30 | .Replace("new URL(\"dotnet.native.wasm\",import.meta.url).href", "\"file:/\"") // when aggressive trimming enabled 31 | .Replace("import.meta.url", url) 32 | .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Bootsharp.Publish; 2 | 3 | internal sealed class ResourceGenerator (string entryAssemblyName, bool embed) 4 | { 5 | private readonly List assemblies = []; 6 | private string wasm = null!; 7 | 8 | public string Generate (string buildDir) 9 | { 10 | foreach (var path in Directory.GetFiles(buildDir, "*.wasm")) 11 | if (path.EndsWith("dotnet.native.wasm")) wasm = BuildBin(path); 12 | else assemblies.Add(BuildBin(path)); 13 | return 14 | $$""" 15 | export default { 16 | wasm: {{wasm}}, 17 | assemblies: [ 18 | {{JoinLines(assemblies, 2, ",\n")}} 19 | ], 20 | entryAssemblyName: "{{entryAssemblyName}}" 21 | }; 22 | """; 23 | } 24 | 25 | private string BuildBin (string path) 26 | { 27 | var name = Path.GetFileName(path); 28 | var content = embed ? ToBase64(File.ReadAllBytes(path)) : "undefined"; 29 | return $$"""{ name: "{{name}}", content: {{content}} }"""; 30 | } 31 | 32 | private string ToBase64 (byte[] bytes) => $"\"{Convert.ToBase64String(bytes)}\""; 33 | } 34 | -------------------------------------------------------------------------------- /src/cs/Bootsharp.Publish/Packer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/cs/Bootsharp/Bootsharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | Bootsharp 6 | Bootsharp 7 | Use C# in web apps with comfort. 8 | NU5100 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/cs/Bootsharp/Build/Bootsharp.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Exe 6 | true 7 | wasm 8 | browser 9 | true 10 | true 11 | true 12 | false 13 | true 14 | 15 | 16 | bootsharp 17 | 18 | true 19 | 20 | false 21 | 22 | none 23 | 24 | false 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <_Parameter1>browser 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/cs/Bootsharp/Build/PackageTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "%MODULE_NAME%", 3 | "type": "module", 4 | "main": "%MODULE_DIR%/index.mjs", 5 | "types": "%TYPES_DIR%/index.d.ts" 6 | } 7 | -------------------------------------------------------------------------------- /src/cs/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.6.3 5 | Elringus 6 | javascript typescript ts js wasm node deno bun interop codegen 7 | https://bootsharp.com 8 | https://github.com/elringus/bootsharp.git 9 | git 10 | logo.png 11 | https://raw.githubusercontent.com/elringus/bootsharp/main/docs/public/favicon.svg 12 | MIT 13 | false 14 | README.md 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/cs/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "compile": "sh scripts/compile-test.sh", 4 | "test": "sh scripts/test.sh", 5 | "cover": "sh scripts/cover.sh", 6 | "build": "sh scripts/build.sh" 7 | }, 8 | "devDependencies": { 9 | "typescript": "5.8.2", 10 | "@types/node": "22.13.14", 11 | "@types/ws": "8.18.0", 12 | "vitest": "3.0.9", 13 | "@vitest/coverage-v8": "3.0.9", 14 | "ws": "8.18.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/js/scripts/build.sh: -------------------------------------------------------------------------------- 1 | rm -rf dist 2 | tsc --outDir dist --declaration 3 | cp src/dotnet.g.d.ts dist/dotnet.g.d.ts 4 | rm dist/*.g.js 5 | -------------------------------------------------------------------------------- /src/js/scripts/compile-test.sh: -------------------------------------------------------------------------------- 1 | cd test/cs 2 | dotnet publish -p BootsharpName=embedded -p BootsharpEmbedBinaries=true -p RunAOTCompilation=true 3 | dotnet publish -p BootsharpName=sideload -p BootsharpEmbedBinaries=false -p RunAOTCompilation=true 4 | -------------------------------------------------------------------------------- /src/js/scripts/cover.sh: -------------------------------------------------------------------------------- 1 | node ./node_modules/vitest/vitest.mjs run \ 2 | --coverage.enabled --coverage.thresholds.100 --coverage.include=**/sideload/*.mjs \ 3 | --coverage.exclude=**/dotnet.* --coverage.allowExternal 4 | -------------------------------------------------------------------------------- /src/js/scripts/test.sh: -------------------------------------------------------------------------------- 1 | node ./node_modules/vitest/vitest.mjs run 2 | -------------------------------------------------------------------------------- /src/js/src/bindings.g.ts: -------------------------------------------------------------------------------- 1 | // Autogenerated and resolved when building C# solution. 2 | export default {}; 3 | -------------------------------------------------------------------------------- /src/js/src/dotnet.native.g.d.ts: -------------------------------------------------------------------------------- 1 | // Resolved when building C# solution. 2 | export const embedded = false; 3 | -------------------------------------------------------------------------------- /src/js/src/dotnet.runtime.g.d.ts: -------------------------------------------------------------------------------- 1 | // Resolved when building C# solution. 2 | export const embedded = false; 3 | -------------------------------------------------------------------------------- /src/js/src/exports.ts: -------------------------------------------------------------------------------- 1 | import type { RuntimeAPI } from "./modules"; 2 | 3 | export let exports: unknown; 4 | 5 | export async function bindExports(runtime: RuntimeAPI, assembly: string) { 6 | const asm = await runtime.getAssemblyExports(assembly); 7 | exports = asm["Bootsharp"]?.["Generated"]["Interop"]; 8 | } 9 | -------------------------------------------------------------------------------- /src/js/src/imports.ts: -------------------------------------------------------------------------------- 1 | import * as bindings from "./bindings.g"; 2 | import { disposeInstance, disposeOnFinalize } from "./instances"; 3 | import type { RuntimeAPI } from "./modules"; 4 | 5 | export function bindImports(runtime: RuntimeAPI) { 6 | runtime.setModuleImports("Bootsharp", { 7 | ...bindings, 8 | disposeInstance, 9 | disposeOnFinalize 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/js/src/index.ts: -------------------------------------------------------------------------------- 1 | import { boot, exit, getStatus, BootStatus } from "./boot"; 2 | import { getMain, getNative, getRuntime } from "./modules"; 3 | import { resources } from "./resources"; 4 | import { buildConfig } from "./config"; 5 | 6 | export default { 7 | boot, 8 | exit, 9 | getStatus, 10 | BootStatus, 11 | resources, 12 | /** .NET internal modules and associated utilities. */ 13 | dotnet: { getMain, getNative, getRuntime, buildConfig } 14 | }; 15 | 16 | export * from "./event"; 17 | export * from "./bindings.g"; 18 | export type { BootOptions } from "./boot"; 19 | export type { BootResources, BinaryResource } from "./resources"; 20 | -------------------------------------------------------------------------------- /src/js/src/instances.ts: -------------------------------------------------------------------------------- 1 | import { exports } from "./exports"; 2 | 3 | const finalizer = new FinalizationRegistry(finalizeInstance); 4 | const idToInstance = new Map(); 5 | const idPool = new Array(); 6 | let nextId = -2147483648; // Number.MIN_SAFE_INTEGER is below C#'s Int32.MinValue 7 | 8 | /* v8 ignore start */ // TODO: Figure how to test finalize/dispose behaviour. 9 | 10 | /** Registers specified imported (JS -> C#) interop instance and associates it with unique ID. 11 | * @param instance Interop instance to resolve ID for. 12 | * @return Unique identifier of the registered instance. */ 13 | export function registerInstance(instance: object): number { 14 | const id = idPool.length > 0 ? idPool.shift()! : nextId++; 15 | idToInstance.set(id, instance); 16 | return id; 17 | } 18 | 19 | /** Resolves registered imported (JS -> C#) interop instance from specified ID. 20 | * @param id Unique identifier of the instance. */ 21 | export function getInstance(id: number): object { 22 | return idToInstance.get(id)!; 23 | } 24 | 25 | /** Invoked from C# to notify that imported (JS -> C#) interop instance is no longer 26 | * used (eg, was garbage collected) and can be released on JavaScript side as well. 27 | * @param id Unique identifier of the disposed interop instance. */ 28 | export function disposeInstance(id: number): void { 29 | idToInstance.delete(id); 30 | idPool.push(id); 31 | } 32 | 33 | /** Registers specified exported (C# -> JS) instance to invoke dispose on C# side 34 | * when it's collected (finalized) by JavaScript runtime GC. 35 | * @param instance Interop instance to register. 36 | * @param id Unique identifier of the interop instance. */ 37 | export function disposeOnFinalize(instance: object, id: number): void { 38 | finalizer.register(instance, id); 39 | } 40 | 41 | function finalizeInstance(id: number) { 42 | (<{ DisposeExportedInstance: (id: number) => void }>exports).DisposeExportedInstance(id); 43 | } 44 | -------------------------------------------------------------------------------- /src/js/src/modules.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleAPI, MonoConfig, AssetEntry } from "./dotnet.g.d.ts"; 2 | 3 | export type * from "./dotnet.g.d.ts"; 4 | export type RuntimeConfig = MonoConfig & { assets?: AssetEntry[] }; 5 | 6 | /** Fetches main dotnet module (dotnet.js). */ 7 | export async function getMain(root?: string): Promise { 8 | if (root == null) return await import("./dotnet.g"); 9 | return await import(/*@vite-ignore*//*webpackIgnore:true*/`${root}/dotnet.js`); 10 | } 11 | 12 | /** Fetches dotnet native module (dotnet.native.js). */ 13 | export async function getNative(root?: string): Promise { 14 | if (root == null) return await import("./dotnet.native.g"); 15 | return await import(/*@vite-ignore*//*webpackIgnore:true*/`${root}/dotnet.native.js`); 16 | } 17 | 18 | /** Fetches dotnet runtime module (dotnet.runtime.js). */ 19 | export async function getRuntime(root?: string): Promise { 20 | if (root == null) return await import("./dotnet.runtime.g"); 21 | return await import(/*@vite-ignore*//*webpackIgnore:true*/`${root}/dotnet.runtime.js`); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/src/resources.g.ts: -------------------------------------------------------------------------------- 1 | import { BootResources } from "./resources"; 2 | 3 | // Autogenerated and resolved when building C# solution. 4 | export default {} as BootResources; 5 | -------------------------------------------------------------------------------- /src/js/src/resources.ts: -------------------------------------------------------------------------------- 1 | import generated from "./resources.g"; 2 | 3 | /** Resources required to boot .NET runtime. */ 4 | export type BootResources = { 5 | /** Compiled .NET WASM runtime module. */ 6 | readonly wasm: BinaryResource; 7 | /** Compiled .NET assemblies. */ 8 | readonly assemblies: BinaryResource[]; 9 | /** Name of the entry (main) assembly, with .dll extension. */ 10 | readonly entryAssemblyName: string; 11 | } 12 | 13 | /** Boot resource with binary content. */ 14 | export type BinaryResource = { 15 | /** Name of the binary file, including extension. */ 16 | readonly name: string; 17 | /** Binary or base64-encoded content of the file; undefined when embedding disabled. */ 18 | readonly content?: Uint8Array | string; 19 | } 20 | 21 | /** Resources required to boot .NET runtime. */ 22 | export const resources: BootResources = generated; 23 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Test.Types; 4 | 5 | public class ExportedInstanced (string instanceArg) : IExportedInstanced 6 | { 7 | public string GetInstanceArg () => instanceArg; 8 | 9 | public async Task GetVehicleIdAsync (Vehicle vehicle) 10 | { 11 | await Task.Delay(1); 12 | return vehicle.Id; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Test.Types; 4 | 5 | public class ExportedStatic : IExportedStatic 6 | { 7 | public async Task GetInstanceAsync (string arg) 8 | { 9 | await Task.Delay(1); 10 | return new ExportedInstanced(arg); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Test.Types; 4 | 5 | public interface IExportedInstanced 6 | { 7 | string GetInstanceArg (); 8 | Task GetVehicleIdAsync (Vehicle vehicle); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Test.Types; 4 | 5 | public interface IExportedStatic 6 | { 7 | Task GetInstanceAsync (string arg); 8 | } 9 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Test.Types; 4 | 5 | public interface IImportedInstanced 6 | { 7 | string GetInstanceArg (); 8 | Task GetVehicleIdAsync (Vehicle vehicle); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Test.Types; 4 | 5 | public interface IImportedStatic 6 | { 7 | Task GetInstanceAsync (string arg); 8 | } 9 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Test.Types.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | true 6 | bin/codegen 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Test.Types; 4 | 5 | public interface IRegistryProvider 6 | { 7 | Registry GetRegistry (); 8 | IReadOnlyList GetRegistries (); 9 | IReadOnlyDictionary GetRegistryMap (); 10 | } 11 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Vehicle/Registry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Bootsharp; 5 | 6 | namespace Test.Types; 7 | 8 | public class Registry 9 | { 10 | public static IRegistryProvider Provider { get; set; } 11 | public List Wheeled { get; set; } 12 | public List Tracked { get; set; } 13 | 14 | [JSInvokable] 15 | public static Registry EchoRegistry (Registry registry) => registry; 16 | 17 | [JSInvokable] 18 | public static float CountTotalSpeed () 19 | { 20 | var registry = Provider.GetRegistry(); 21 | return registry.Tracked.Sum(t => t.MaxSpeed) + 22 | registry.Wheeled.Sum(t => t.MaxSpeed); 23 | } 24 | 25 | [JSInvokable] 26 | public static async Task> ConcatRegistriesAsync (IReadOnlyList registries) 27 | { 28 | await Task.Delay(1); 29 | return registries.Concat(Provider.GetRegistries()).ToArray(); 30 | } 31 | 32 | [JSInvokable] 33 | public static async Task> MapRegistriesAsync (IReadOnlyDictionary map) 34 | { 35 | await Task.Delay(1); 36 | return map.Concat(Provider.GetRegistryMap()).ToDictionary(kv => kv.Key, kv => kv.Value); 37 | } 38 | 39 | [JSInvokable] 40 | public static Vehicle GetWithEmptyId () => new() { Id = "" }; 41 | } 42 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Vehicle/TrackType.cs: -------------------------------------------------------------------------------- 1 | namespace Test.Types; 2 | 3 | public enum TrackType 4 | { 5 | Rubber, 6 | Chain 7 | } 8 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Vehicle/Tracked.cs: -------------------------------------------------------------------------------- 1 | namespace Test.Types; 2 | 3 | public class Tracked : Vehicle 4 | { 5 | public TrackType TrackType { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Vehicle/Vehicle.cs: -------------------------------------------------------------------------------- 1 | namespace Test.Types; 2 | 3 | public class Vehicle 4 | { 5 | public string Id { get; set; } 6 | public float MaxSpeed { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.Types/Vehicle/Wheeled.cs: -------------------------------------------------------------------------------- 1 | namespace Test.Types; 2 | 3 | public class Wheeled : Vehicle 4 | { 5 | public int WheelCount { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/js/test/cs/Test.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test/Test.csproj", "{B6CC139C-70A6-4B48-8344-5AC58253C803}" 3 | EndProject 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Types", "Test.Types/Test.Types.csproj", "{B66B3A46-37FA-4B13-8783-4FAA096DDD0A}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {B6CC139C-70A6-4B48-8344-5AC58253C803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {B6CC139C-70A6-4B48-8344-5AC58253C803}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {B6CC139C-70A6-4B48-8344-5AC58253C803}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {B6CC139C-70A6-4B48-8344-5AC58253C803}.Release|Any CPU.Build.0 = Release|Any CPU 16 | {B66B3A46-37FA-4B13-8783-4FAA096DDD0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B66B3A46-37FA-4B13-8783-4FAA096DDD0A}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B66B3A46-37FA-4B13-8783-4FAA096DDD0A}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B66B3A46-37FA-4B13-8783-4FAA096DDD0A}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | EndGlobal 22 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/Event.cs: -------------------------------------------------------------------------------- 1 | using Bootsharp; 2 | using Test.Types; 3 | 4 | namespace Test; 5 | 6 | public static partial class Event 7 | { 8 | [JSInvokable] 9 | public static void BroadcastEvent (string value) => OnEvent(value); 10 | 11 | [JSInvokable] 12 | public static void BroadcastEventMultiple (byte num, Vehicle? vehicle, TrackType type) => OnEventMultiple(num, vehicle, type); 13 | 14 | [JSEvent] 15 | public static partial string OnEvent (string value); 16 | 17 | [JSEvent] 18 | public static partial string OnEventMultiple (byte num, Vehicle? vehicle, TrackType type); 19 | } 20 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/Functions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Bootsharp; 4 | 5 | namespace Test; 6 | 7 | public static partial class Functions 8 | { 9 | [JSInvokable] 10 | public static string EchoString () => GetString(); 11 | 12 | [JSFunction] 13 | public static partial string GetString (); 14 | 15 | [JSInvokable] 16 | public static async Task EchoStringAsync () 17 | { 18 | await Task.Delay(1); 19 | return await GetStringAsync(); 20 | } 21 | 22 | [JSFunction] 23 | public static partial Task GetStringAsync (); 24 | 25 | [JSInvokable] 26 | public static byte[] EchoBytes () => GetBytes(); 27 | 28 | [JSFunction] 29 | public static partial byte[] GetBytes (); 30 | 31 | [JSInvokable] 32 | public static async Task EchoBytesAsync (byte[] arr) 33 | { 34 | await Task.Delay(1); 35 | return arr; 36 | } 37 | 38 | [JSInvokable] 39 | public static IList EchoColExprString (IList list) 40 | { 41 | return [..list]; 42 | } 43 | 44 | [JSInvokable] 45 | public static IReadOnlyList EchoColExprDouble (IReadOnlyList list) 46 | { 47 | return [..list]; 48 | } 49 | 50 | [JSInvokable] 51 | public static ICollection EchoColExprInt (ICollection list) 52 | { 53 | return [..list]; 54 | } 55 | 56 | [JSInvokable] 57 | public static IReadOnlyCollection EchoColExprByte (IReadOnlyCollection list) 58 | { 59 | return [..list]; 60 | } 61 | 62 | [JSInvokable] 63 | public static string[] EchoStringArray (string[] arr) => arr; 64 | [JSInvokable] 65 | public static double[] EchoDoubleArray (double[] arr) => arr; 66 | [JSInvokable] 67 | public static int[] EchoIntArray (int[] arr) => arr; 68 | [JSInvokable] 69 | public static byte[] EchoByteArray (byte[] arr) => arr; 70 | } 71 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/IdxEnum.cs: -------------------------------------------------------------------------------- 1 | namespace Test; 2 | 3 | public enum IdxEnum 4 | { 5 | One = 1, 6 | Two = 2 7 | } 8 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/Invokable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using Bootsharp; 5 | 6 | namespace Test; 7 | 8 | public static class Invokable 9 | { 10 | [JSInvokable] 11 | public static void InvokeVoid () { } 12 | 13 | [JSInvokable] 14 | public static string JoinStrings (string a, string b) => a + b; 15 | 16 | [JSInvokable] 17 | public static double SumDoubles (double a, double b) => a + b; 18 | 19 | [JSInvokable] 20 | public static DateTime AddDays (DateTime date, int days) => date.AddDays(days); 21 | 22 | [JSInvokable] 23 | public static async Task JoinStringsAsync (string a, string b) 24 | { 25 | await Task.Delay(1).ConfigureAwait(false); 26 | return a + b; 27 | } 28 | 29 | [JSInvokable] 30 | public static string BytesToString (byte[] bytes) => Encoding.UTF8.GetString(bytes); 31 | 32 | [JSInvokable] 33 | public static IdxEnum GetIdxEnumOne () => IdxEnum.One; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/Platform.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Bootsharp; 7 | 8 | namespace Test; 9 | 10 | public static partial class Platform 11 | { 12 | [JSInvokable] 13 | public static string GetGuid () => Guid.NewGuid().ToString(); 14 | 15 | [JSInvokable] 16 | public static string? CatchException () 17 | { 18 | try { ThrowJS(); } 19 | catch (Exception e) { return e.Message; } 20 | return null; 21 | } 22 | 23 | [JSInvokable] 24 | public static void ThrowCS (string message) => throw new Exception(message); 25 | 26 | [JSFunction] 27 | public static partial void ThrowJS (); 28 | 29 | [JSInvokable] 30 | public static async Task EchoWebSocket (string uri, string message, int timeout) 31 | { 32 | using var cts = new CancellationTokenSource(timeout); 33 | using var ws = new ClientWebSocket(); 34 | await ws.ConnectAsync(new Uri(uri), cts.Token); 35 | var buffer = Encoding.UTF8.GetBytes(message); 36 | await ws.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token); 37 | await ws.ReceiveAsync(new ArraySegment(buffer), cts.Token); 38 | return Encoding.UTF8.GetString(buffer); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bootsharp; 4 | using Bootsharp.Inject; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Test.Types; 7 | 8 | [assembly: JSExport(typeof(IExportedStatic))] 9 | [assembly: JSImport(typeof(IImportedStatic), typeof(IRegistryProvider))] 10 | 11 | namespace Test; 12 | 13 | public static partial class Program 14 | { 15 | private static IServiceProvider services = null!; 16 | 17 | public static void Main () 18 | { 19 | services = new ServiceCollection() 20 | .AddSingleton() 21 | .AddBootsharp() 22 | .BuildServiceProvider() 23 | .RunBootsharp(); 24 | Registry.Provider = services.GetRequiredService(); 25 | OnMainInvoked(); 26 | } 27 | 28 | [JSFunction] 29 | public static partial void OnMainInvoked (); 30 | 31 | [JSInvokable] 32 | public static async Task GetExportedArgAndVehicleIdAsync (Vehicle vehicle, string arg) 33 | { 34 | var exported = services.GetService()!; 35 | var instance = await exported.GetInstanceAsync(arg); 36 | return await instance.GetVehicleIdAsync(vehicle) + instance.GetInstanceArg(); 37 | } 38 | 39 | [JSInvokable] 40 | public static async Task GetImportedArgAndVehicleIdAsync (Vehicle vehicle, string arg) 41 | { 42 | var imported = services.GetService()!; 43 | var instance = await imported.GetInstanceAsync(arg); 44 | return await instance.GetVehicleIdAsync(vehicle) + instance.GetInstanceArg(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/js/test/cs/Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | browser-wasm 7 | true 8 | bin/codegen 9 | false 10 | npx rollup index.js -d ./ -f es -g process,module --output.preserveModules --entryFileNames [name].mjs 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/js/test/cs/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/js/test/spec/export.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { embedded, getDeclarations } from "../cs"; 3 | 4 | describe("export", () => { 5 | it("exports bootsharp api", () => { 6 | expect(embedded.boot).toBeTypeOf("function"); 7 | expect(embedded.exit).toBeTypeOf("function"); 8 | expect(embedded.resources).toBeTypeOf("object"); 9 | }); 10 | it("exports dotnet modules", () => { 11 | expect(embedded.dotnet.getMain).toBeTypeOf("function"); 12 | expect(embedded.dotnet.getNative).toBeTypeOf("function"); 13 | expect(embedded.dotnet.getRuntime).toBeTypeOf("function"); 14 | }); 15 | it("exports type declarations", () => { 16 | expect(getDeclarations()).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/js/test/spec/platform.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, expect } from "vitest"; 2 | import { WebSocket, WebSocketServer } from "ws"; 3 | import { Test, bootSideload, any } from "../cs"; 4 | 5 | describe("platform", () => { 6 | beforeAll(bootSideload); 7 | it("can provide unique guid", () => { 8 | const guid1 = Test.Platform.getGuid(); 9 | const guid2 = Test.Platform.getGuid(); 10 | expect(guid1.length).toStrictEqual(36); 11 | expect(guid2.length).toStrictEqual(36); 12 | expect(guid1).not.toStrictEqual(guid2); 13 | }); 14 | it("can communicate via websocket", async () => { 15 | // .NET requires ws package when running on node: 16 | // https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md#websocket 17 | any(global).WebSocket = WebSocket; 18 | const wss = new WebSocketServer({ port: 8877 }); 19 | wss.on("connection", socket => socket.on("message", socket.send)); 20 | expect(await Test.Platform.echoWebSocket("ws://localhost:8877", "foo", 3000)).toStrictEqual("foo"); 21 | wss.close(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true 8 | }, 9 | "include": ["src"] 10 | } 11 | --------------------------------------------------------------------------------