diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4893f25..de90cf7 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,12 +1,15 @@
name: Main
on:
- workflow_dispatch:
workflow_call:
+ outputs:
+ badge-color:
+ description: 'Color for the test results badge'
+ value: ${{ jobs.publish-results.outputs.badge-color }}
+ badge-message:
+ description: 'Text for the test results badge'
+ value: ${{ jobs.publish-results.outputs.badge-message }}
pull_request:
- push:
- branches:
- - master
permissions:
checks: write
@@ -98,32 +101,15 @@ jobs:
!**/*.dSYM/**
if-no-files-found: error
retention-days: 1
-
- combine:
- name: ๐ฆ Publish Artifacts
- needs: build
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Download all RID artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: pack-build-*
- path: pack
- merge-multiple: true
-
- - name: Upload combined pack directory
- uses: actions/upload-artifact@v4
- with:
- name: typeshim-pack
- path: pack/**
publish-results:
name: ๐งพ Publish Results
runs-on: ubuntu-latest
needs: build
if: ${{ always() && !cancelled() }}
+ outputs:
+ badge-color: ${{ steps.badge.outputs.color }}
+ badge-message: ${{ steps.badge.outputs.message }}
steps:
- name: Download test results
uses: actions/download-artifact@v4
@@ -140,28 +126,13 @@ jobs:
results/**/e2e-report-*.xml
- name: Determine badge content
- if: ${{ github.event_name == 'push' }}
- shell: bash
+ id: badge
+ shell: pwsh
run: |
- conclusion='${{ fromJSON(steps.test-results.outputs.json).conclusion }}'
- if [ "$conclusion" = "success" ]; then
- echo "BADGE_COLOR=#26cc23" >> "$GITHUB_ENV"
- echo "BADGE_MESSAGE=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_succ }} passing" >> "$GITHUB_ENV"
- else
- echo "BADGE_COLOR=#cc5623" >> "$GITHUB_ENV"
- echo "BADGE_MESSAGE=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_fail }}/${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} failing" >> "$GITHUB_ENV"
- fi
-
- - name: Update badge
- if: ${{ github.event_name == 'push' }}
- uses: schneegans/dynamic-badges-action@v1.7.0
- with:
- auth: ${{ secrets.GIST_TOKEN }}
- gistID: '0f24ed28316a25f6293d5771a247f19d'
- filename: typeshim-tests-badge.json
- label: Tests
- message: ${{ env.BADGE_MESSAGE }}
- color: ${{ env.BADGE_COLOR }}
+ $path = if ("${{ fromJSON(steps.test-results.outputs.json).conclusion }}" -eq "success") { '#26cc23' } else { '#cc5623' }
+ $message = if ("${{ fromJSON(steps.test-results.outputs.json).conclusion }}" -eq "success") { "${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_succ }} passing" } else { "${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_fail }}/${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} failing" }
+ "color=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
+ "message=$message" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
benchmarks:
name: ๐ Benchmark Generator
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 65480e2..71ced48 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,15 +1,15 @@
-name: Publish NuGet
+name: Publish
on:
workflow_dispatch:
inputs:
version:
- description: 'Tag to publish v[0-9]+.[0-9]+.[0-9]+*'
+ description: 'Tag to publish v1.2.3[-prerelease]'
required: true
default: ''
type: string
dry-run:
- description: 'Perform a dry run without pushing to NuGet'
+ description: 'Dry run'
required: false
default: false
type: boolean
@@ -17,26 +17,17 @@ on:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
-jobs:
- build:
- name: ๐ ๏ธ Build and Pack
- uses: ./.github/workflows/main.yml
+permissions:
+ checks: write
+ pull-requests: write
- publish:
- name: ๐ฆ Publish to NuGet
- needs: build
+jobs:
+ version:
+ name: Determine Version
runs-on: ubuntu-latest
- permissions:
- contents: read
+ outputs:
+ version: ${{ steps.ver.outputs.version }}
steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- global-json-file: src/global.json
-
- name: Determine and validate version
id: ver
shell: pwsh
@@ -63,27 +54,55 @@ jobs:
Write-Host "Version: $ver"
"version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- - name: Download pack artifact
+ build:
+ name: Main
+ uses: ./.github/workflows/main.yml
+
+ publish:
+ name: ๐ฆ Publish to NuGet
+ needs: [version, build]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: src/global.json
+
+ - name: Restore
+ run: dotnet restore src/TypeShim/TypeShim.csproj
+
+ - name: Download all RID artifacts
uses: actions/download-artifact@v4
with:
- pattern: typeshim-pack
+ pattern: pack-build-*
path: src/TypeShim/bin/pack
+ merge-multiple: true
+
+ - name: Build TypeShim library
+ run: >
+ dotnet build src/TypeShim/TypeShim.csproj
+ -c Release
+ --no-restore
- name: Pack TypeShim
- env:
- PKG_VERSION: ${{ steps.ver.outputs.version }}
run: >
dotnet pack src/TypeShim/TypeShim.csproj
-c Release
- -o .\.artifacts
+ -o .
--no-build
- -p:Version=$env:PKG_VERSION
+ -p:Version=${{ needs.version.outputs.version }}
-p:ContinuousIntegrationBuild=true
- name: Push to GitHub Packages
if: ${{ !inputs.dry-run }}
run: >
- dotnet nuget push .\.artifacts\*.nupkg
+ dotnet nuget push
+ TypeShim.${{ needs.version.outputs.version }}.nupkg
-k ${{ secrets.NUGET_KEY }}
-s https://api.nuget.org/v3/index.json
@@ -91,5 +110,20 @@ jobs:
if: ${{ inputs.dry-run }}
uses: actions/upload-artifact@v4
with:
- name: typeshim-nuget-package-${{ steps.ver.outputs.version }}
- path: .\.artifacts\*.nupkg
\ No newline at end of file
+ name: typeshim-nupkg-${{ needs.version.outputs.version }}
+ path: TypeShim.${{ needs.version.outputs.version }}.nupkg
+
+ badge:
+ name: ๐ท๏ธ Update Readme Badge
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Update badge
+ uses: schneegans/dynamic-badges-action@v1.7.0
+ with:
+ auth: ${{ secrets.GIST_TOKEN }}
+ gistID: '0f24ed28316a25f6293d5771a247f19d'
+ filename: typeshim-tests-badge.json
+ label: Tests
+ message: ${{ needs.build.outputs.badge-message }}
+ color: ${{ needs.build.outputs.badge-color }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 321d62d..8db3795 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,22 @@
TypeShim
- Strongly-typed .NET-JS interop facade generation
+ Bridge .NET WebAssembly and TypeScript with fast, reliable, and type-safe codegen.
## Why TypeShim
-With TypeShim you can use your .NET WASM project with TypeScript _without having to write a single JSExport yourself_. All interop code (C# & TS) is generated at build-time to provide reliability and type safety. TypeShim's class level exports make for a natural programming experience with familiar syntax. No bells and whistles, just your .NET WASM project accessible through a neat TypeScript facade.
+With TypeShim you can interop between .NET WebAssembly and TypeScript with class level exports. It takes just one `[TSExport]` to bring your class to TypeScript with all its constructors, methods and properties. TypeShim aims to provide a natural programming experience with familiar syntax, without any unnecessary bells and whistles. It just makes your .NET classes available in TypeScript, no hassle.
-Simply install TypeShim in your .NET WASM project, drop a `[TSExport]` on your C# classes and _voilร _: .NET-JS interop is generated at build time including a powerful TypeScript library matching your C# classes exactly.
+The interop code on both the C# & TypeScript side is build-time generated in a flash to provide an up-to-date and type safe interop boundary that just _works_. Thoroughly tested from codegen to runtime to deliver reliability.
-## Features at a glance
+## At a glance
-- ๐ญ No-nonsense [interop](#concepts) generation.
-- ๐ฑ Opt-in with just one attribute.
-- ๐ค Export full classes: constructors, methods and properties.
-- ๐ฐ [Enriched type marshalling](#enriched-type-support).
-- ๐ก Type-safety across the interop boundary.
+- ๐ค Class level exports.
+- ๐ [Rich type support](#enriched-type-support).
+- โ No-nonsense [interop](#concepts) codegen.
+- ๐ฆพ Thoroughly validated for correctness
+- โก Tuned for [high performance](#performance)
- ๐ [Easy setup](#installing)
## Samples
@@ -294,6 +294,8 @@ TypeShim aims to continue to broaden its type support. Suggestions and contribut
| `Dictionary` | `?` | ๐ก | under consideration |
| `(T1, T2)` | `[T1, T2]` | ๐ก | under consideration |
+Table 1. TypeShim supported interop types
+
| .NET Marshalled Type | Mapped Type | Support | Note |
|----------------------|-------------|--------|------|
| `Boolean` | `Boolean` | โ
| |
@@ -328,6 +330,8 @@ TypeShim aims to continue to broaden its type support. Suggestions and contribut
| `Func` | `Function`| โ
| |
| `Func` | `Function` | โ
| |
+Table 2. TypeShim support for .NET-JS interop types
+
*For `[TSExport]` classes
## Run the sample
@@ -357,6 +361,8 @@ TypeShim is configured through MSBuild properties, you may provide these through
| `TypeShim_GeneratedDir` | `TypeShim` | Directory path (relative to `IntermediateOutputPath`) for generated `YourClass.Interop.g.cs` files. | `TypeShim` |
| `TypeShim_MSBuildMessagePriority` | `Normal` | MSBuild message priority. Set to High for debugging. | `Low`, `Normal`, `High` |
+Table 3. Configuration options
+
### Limitations
TSExports are subject to minimal, but some, constraints.
@@ -365,6 +371,35 @@ TSExports are subject to minimal, but some, constraints.
- By default, JSExport yields value semantics for Array instances, this is one reference type that is atypical. It is under consideration to be addressed but an effective alternative is to define your own List class to preserve reference semantics.
- Classes with generic type parameters can not be part of interop codegen at this time.
+### Performance
+
+TypeShim has been optimized to achieve average codegen times of ~1 ms per class in a set of benchmarks going up to 200 classes. By optimizing the implementation and providing NativeAOT builds via the NuGet package, most users should see end-to-end codegen times of roughly 50โ200 ms for projects with 25โ200 classes. Every PR validates both AOT and JIT performance to help maintain these numbers.
+
+Performance is prioritized to minimize build-time impact and deliver the best possible experience for TypeShim users. Secondly it was a good excuse to play around with profiling tools and get some hands on experience with performance optimization and NativeAOT.
+
+The earlier versions of TypeShim used regular JIT builds which suffered expensive runtime start times and an inability to warm-up so even smaller projects would require more than 1 second for codegen. Switching to NativeAOT brought this down to the quarterisecond range and after several optimizations has been reduced to below a tenth of a second in many cases.
+
+Results from the continuous benchmarking that is now part of every pull request are shown in Table 4. The 0 classes case demonstrates the overhead of starting the process without doing any work.
+
+| Method | Compilation | ClassCount | Mean | Error | StdDev |
+|--------- |------------ |-----------:|------------:|----------:|----------:|
+| **Generate** | **AOT** | **0** | **14.02 ms** | **1.319 ms** | **0.873 ms** |
+| **Generate** | **AOT** | **1** | **31.35 ms** | **0.969 ms** | **0.641 ms** |
+| **Generate** | **AOT** | **10** | **31.82 ms** | **1.683 ms** | **1.113 ms** |
+| **Generate** | **AOT** | **25** | **45.32 ms** | **1.565 ms** | **1.035 ms** |
+| **Generate** | **AOT** | **50** | **56.50 ms** | **1.103 ms** | **0.730 ms** |
+| **Generate** | **AOT** | **100** | **91.60 ms** | **2.294 ms** | **1.517 ms** |
+| **Generate** | **AOT** | **200** | **93.92 ms** | **1.553 ms** | **1.027 ms** |
+| **Generate** | **JIT** | **0** | **42.07 ms** | **0.687 ms** | **0.454 ms** |
+| **Generate** | **JIT** | **1** | **813.62 ms** | **10.321 ms** | **6.827 ms** |
+| **Generate** | **JIT** | **10** | **814.93 ms** | **9.107 ms** | **6.024 ms** |
+| **Generate** | **JIT** | **25** | **862.08 ms** | **11.549 ms** | **7.639 ms** |
+| **Generate** | **JIT** | **50** | **900.00 ms** | **14.144 ms** | **9.355 ms** |
+| **Generate** | **JIT** | **100** | **1,014.10 ms** | **12.046 ms** | **7.968 ms** |
+| **Generate** | **JIT** | **200** | **986.96 ms** | **22.021 ms** | **14.565 ms** |
+
+Table 4. Benchmark results on an AMD EPYC 7763 2.45GHz Github Actions runner.
+
## Contributing
Contributions are welcome.
diff --git a/src/TypeShim/TsExportAttribute.cs b/src/TypeShim/TsExportAttribute.cs
index ea518be..01a05af 100644
--- a/src/TypeShim/TsExportAttribute.cs
+++ b/src/TypeShim/TsExportAttribute.cs
@@ -4,25 +4,27 @@
/// TSExport classes must be non-static.
///
///
-/// TSExport classes define your interop API surface. You can use static and instance members in your TSExport classes, you will use them as static or instance in TypeScript too.
-/// Whenever a method returns an instance of a TSExport class, TypeShim generates a TypeScript proxy class that wraps the interop calls to the underlying C# instance.
+/// TSExport classes define your .NET-TS API surface. Public methods, properties and constructors, both static and member, are accessible from the generated TypeScript library.
+/// Whenever a TSExport class crosses the interop boundary, TypeShim automatically wraps it in an appropriate TypeScript class matching the C# class's public signature.
///
/// [TSExport]
/// public class MyCounter
/// {
-/// public static MyCounter GetCounter() => new MyCounter();
/// public int Count { get; private set; } = 0;
/// public string Increment() => $"Hello from C#, Count is now {++Count}";
+/// public bool EqualsCount(MyCounter other) => Count == other.Count;
/// }
///
///
-/// Usage in TypeScript looks like this:
+/// Can be used in TypeScript like this:
///
///
-/// const counter: MyCounter.Proxy = MyCounter.GetCounter();
+/// const counter: MyCounter = new MyCounter();
/// console.log(counter.Count); // 0
/// console.log(counter.Increment()); // "Hello from C#, Count is now 1"
/// console.log(counter.Count); // 1
+/// const counter2: MyCounter = new MyCounter();
+/// console.log(counter.EqualsCount(counter2)); // false
///
///
[AttributeUsage(AttributeTargets.Class)]
diff --git a/src/TypeShim/TypeShim.csproj b/src/TypeShim/TypeShim.csproj
index 204e0b9..7f4afdf 100644
--- a/src/TypeShim/TypeShim.csproj
+++ b/src/TypeShim/TypeShim.csproj
@@ -7,7 +7,9 @@
enable
enable
en
-
+ none
+ false
+
TypeShim
@@ -15,13 +17,15 @@
ArcadeMode
TypeShim
Typesafe .NET โ๏ธ TypeScript interop
- wasm, csharp, typescript, interop, generated
+ wasm, csharp, typescript, interop, generator
https://github.com/ArcadeMode/TypeShim
https://github.com/ArcadeMode/TypeShim
MIT
+ README.md
+
diff --git a/src/TypeShim/TypeShim.targets b/src/TypeShim/TypeShim.targets
index c9b5fa9..bb0ecbb 100644
--- a/src/TypeShim/TypeShim.targets
+++ b/src/TypeShim/TypeShim.targets
@@ -60,7 +60,7 @@
-
+
<_TypeShim_TypeScriptOutputFilePathSegment>$([System.IO.Path]::Combine('$(TypeShim_TypeScriptOutputDirectory)', '$(TypeShim_TypeScriptOutputFileName)'))
@@ -68,8 +68,8 @@
<_TypeShim_CSharpOutputDirectoryPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)', '$(TypeShim_GeneratedDir)'))
-
-
+
+
@@ -80,11 +80,11 @@
Command=""$(_TypeShimGeneratorExePath)" "@(Compile->'%(FullPath)', ';')" "$(_TypeShim_CSharpOutputDirectoryPath)" "$(_TypeShim_TypeScriptOutputFilePath)" "$(_TargetingPackRefDir)""
WorkingDirectory="$(ProjectDir)"
IgnoreExitCode="false" />
-
+
-
+
@@ -98,6 +98,6 @@
-
+
\ No newline at end of file