π A tool for seamless Blazor-TypeScript integration
English | δΈζ | π GitHub
BlazorTS is a source generator library that uses Tree-sitter syntax tree parsing to analyze TypeScript code and automatically generate strongly typed C# wrappers, allowing Blazor applications to call TypeScript functions directly without hand-written JavaScript interop glue.
- π Compile-time generation: Analyze TypeScript files and generate C# wrappers automatically
- π― Type safety: Strongly typed C# APIs for exported TypeScript functions
- π Zero configuration: Minimal setup, works out of the box
- π§ Automatic registration: Generated interop services are registered through
AddBlazorTSScripts() - π³ Precise parsing: Uses Tree-sitter for accurate TypeScript syntax handling
| NuGet Package | Description |
|---|---|
BlazorTS |
Core runtime library |
BlazorTS.SourceGenerator |
Source generator library |
BlazorTS.Assets |
Asset management with dynamic URLs |
Microsoft.TypeScript.MSBuild |
TypeScript compilation support |
dotnet add package BlazorTS
dotnet add package BlazorTS.SourceGenerator
dotnet add package BlazorTS.Assets # optional
dotnet add package Microsoft.TypeScript.MSBuild # optional<ItemGroup>
<AdditionalFiles Include="**/*.razor.ts" Exclude="**/node_modules/**" />
<AdditionalFiles Include="**/*.entry.ts" Exclude="**/node_modules/**" />
</ItemGroup>These files are bound to a specific Razor component.
- Naming:
MyComponent.razor.tsmust pair withMyComponent.razor - Generated output:
- a
partial classfor the component - a top-level
*TSInteropservice for that component - an auto-injected
Scriptsproperty on the component
- a
- Usage: call exported TypeScript functions through the generated
Scriptsproperty
Example
// Components/Pages/Counter.razor.ts
export function increment(count: number): number {
return count + 1;
}@page "/counter"
@code {
private int currentCount = 0;
private async Task HandleClick()
{
currentCount = await Scripts.increment(currentCount);
}
}Conceptually, BlazorTS generates code like this:
public partial class Counter
{
[Inject] public CounterTSInterop Scripts { get; set; } = null!;
}
public class CounterTSInterop(ScriptBridge invoker)
{
private readonly string url = invoker.ResolveNS("MyApp.Components.Pages.Counter", ".razor");
public async Task<double> increment(double count)
{
return await invoker.InvokeAsync<double>(url, "increment", new object?[] { count });
}
}These files define reusable TypeScript modules that can be injected as services.
- Naming:
my-utils.entry.tsorapi.entry.ts - Generated output: a normal C# service class
- Usage: register with
AddBlazorTSScripts()and inject where needed
// Services/Formatter.entry.ts
export function formatCurrency(amount: number): string {
return `$${amount.toFixed(2)}`;
}builder.Services.AddBlazorTSScripts();@inject TestApp.Services.Formatter Formatter
<p>@await Formatter.formatCurrency(123.45)</p>{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"target": "es2015",
"rootDir": ".",
"outDir": "wwwroot/js"
},
"include": [
"**/*.razor.ts",
"**/*.entry.ts"
]
}using BlazorTS.SourceGenerator.Extensions;
builder.Services.AddBlazorTS();
builder.Services.AddBlazorTSScripts();By default, BlazorTS maps a logical component or module name to /js/....
builder.Services.AddBlazorTS((type, suffix) =>
{
var path = type.FullName!.Replace('.', '/');
return $"/scripts/{path}{suffix}.js";
});public class CustomResolver : INSResolver
{
public string ResolveNS(Type tsType, string suffix)
{
var path = tsType.FullName!.Replace('.', '/');
return $"/lib/{path}{suffix}.js";
}
public string ResolveNS(string fullName, string suffix)
{
var path = fullName.Replace('.', '/');
return $"/lib/{path}{suffix}.js";
}
}
builder.Services.AddBlazorTS<CustomResolver>();- Razor components (
.razor.ts) use suffix".razor" - Entry modules (
.entry.ts) use suffix".entry" - Other module styles can use any custom suffix
| TypeScript | C# Parameter | Return Type |
|---|---|---|
string |
string |
Task<string> |
number |
double |
Task<double> |
boolean |
bool |
Task<bool> |
any |
object? |
Task<object?> |
void |
- | Task |
Promise<T> |
- | Task<T> |
The repository now includes a minimal real Blazor app at BlazorTS.RealBlazorApp/ to validate behavior beyond Roslyn-only generator tests.
BlazorTS.RealBlazorApp/: a minimal real Blazor Web App that keeps only the files required for interop verificationBlazorTS.RealBlazorApp/Components/Pages/GenericProbe.razor: a real generic Razor componentBlazorTS.RealBlazorApp/Components/Pages/GenericProbe.razor.ts: the paired script that generates the top-levelGenericProbeTSInteropBlazorTS.RealBlazorApp.Tests/: regression tests that assert the finalized model through reflection, DI registration, and real compiled artifacts
This project specifically covers:
- generic parameters stay on the component partial class
Scriptsauto-injection still works in a real Razor compilation flow- interop services remain top-level non-generic
*TSInteroptypes AddBlazorTSScripts()registers the generated interop type for DI
Issues and Pull Requests are welcome.
MIT License