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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
Expand Down
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,6 @@ Table 2. TypeShim support for .NET-JS interop types

*<sub>For `[TSExport]` classes</sub>

## Run the sample

To build and run the project:
```
cd Sample/TypeShim.Sample.Client && npm install && npm run build && cd ../TypeShim.Sample.Server && dotnet run
```
The app should be available on [http://localhost:5012](http://localhost:5012)

## <a name="installing"></a>Installing

To use TypeShim all you have to do is install it directly into your `Microsoft.NET.Sdk.WebAssembly`-powered project. Check the [configuration](#configuration) section for configuration you might want to adjust to your project.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ temp/
# Preserve root lock
!package-lock.json
# If individual workspace package-locks accidentally get created (npm -w install inside):
@typeshim/*/package-lock.json
@client/*/package-lock.json

# -----------------------------
# Misc bundler artifacts
Expand Down
2 changes: 1 addition & 1 deletion sample/TypeShim.Sample/Dtos.cs → sample/Library/Dtos.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TypeShim.Sample;
namespace Client.Library;

public class PeopleDto
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,35 @@
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<Nullable>enable</Nullable>

<WasmFingerprintAssets>true</WasmFingerprintAssets>
<WasmBundlerFriendlyBootConfig>true</WasmBundlerFriendlyBootConfig>
<WasmFingerprintAssets>false</WasmFingerprintAssets>
<CompressionEnabled>false</CompressionEnabled>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>

<ItemGroup>
<StaticWebAssetFingerprintPattern Include="JS" Pattern="*.js" Expression="#[.{fingerprint}]!" />
</ItemGroup>

<PropertyGroup>
<OutputPath>./bin/</OutputPath>
<!--<TypeShim_GeneratedDir>../../../TypeShim</TypeShim_GeneratedDir>-->
<TypeShim_TypeScriptOutputDirectory>../../../../TypeShim.Sample.Client/@typeshim/wasm-exports</TypeShim_TypeScriptOutputDirectory>
<TypeShim_MSBuildMessagePriority>High</TypeShim_MSBuildMessagePriority>
<!--<TypeShim_TypeScriptOutputDirectory>wwwroot</TypeShim_TypeScriptOutputDirectory>-->
<!--<TypeShim_MSBuildMessagePriority>High</TypeShim_MSBuildMessagePriority>-->
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="TypeShim" />

<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" />
</ItemGroup>

<PropertyGroup>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
<Content Include="wwwroot\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System;
using System.Threading.Tasks;
using TypeShim;

namespace TypeShim.Sample;
namespace Client.Library;

[TSExport]
public class People()
Expand Down
28 changes: 28 additions & 0 deletions sample/Library/PeopleApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Client.Library;

public class PeopleApiClient(HttpClient httpClient)
{
public async Task<IEnumerable<Person>> GetAllPeopleAsync()
{
PeopleDto? dto = await httpClient.GetFromJsonAsync("/people/all", typeof(PeopleDto), PersonDtoSerializerContext.Default) as PeopleDto;
return dto?.People?.Select(dto => dto.ToPerson()) ?? [];
}
}

[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Serialization | JsonSourceGenerationMode.Metadata,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(PeopleDto))]
[JsonSerializable(typeof(PersonDto))]
[JsonSerializable(typeof(PersonDto[]))]
[JsonSerializable(typeof(DogDto))]
[JsonSerializable(typeof(DogDto[]))]
internal partial class PersonDtoSerializerContext : JsonSerializerContext { }
37 changes: 37 additions & 0 deletions sample/Library/PeopleApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Net.Http;
using TypeShim;

namespace Client.Library;

[TSExport]
public class PeopleAppOptions
{
public required string BaseAddress { get; init; }
}

[TSExport]
public class PeopleApp
{
private readonly IHost _host;

public PeopleApp(PeopleAppOptions options)
{
// we dont -need- a servicecollection for this demo but its here to show you can use anything on the .net side
_host = new HostBuilder().ConfigureServices(services =>
{
services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(options.BaseAddress) });
services.AddSingleton<PeopleApiClient>();
services.AddSingleton<PeopleProvider>(sp => new PeopleProvider(sp.GetRequiredService<PeopleApiClient>()));
}).Build();
Console.WriteLine($".NET {nameof(PeopleApp)} Constructor completed");
}

public PeopleProvider GetPeopleProvider()
{
return _host.Services.GetRequiredService<PeopleProvider>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Threading.Tasks;
using TypeShim;

namespace TypeShim.Sample;
namespace Client.Library;

[TSExport]
public class PeopleProvider
Expand All @@ -17,18 +17,10 @@ internal PeopleProvider(PeopleApiClient apiClient)
_apiClient = apiClient;
}

public Person[]? PeopleCache => AllPeople;
public Task<TimeoutUnit?>? DelayTask { get; set; } = null;

public async Task<People> FetchPeopleAsync()
{
try
{
if (DelayTask != null)
{
await Task.Delay((await DelayTask)?.Timeout ?? 0);
}

if (AllPeople == null)
{
AllPeople = [.. await _apiClient.GetAllPeopleAsync()];
Expand All @@ -46,11 +38,4 @@ public async Task<People> FetchPeopleAsync()
throw; // hand over to js
}
}
}


[TSExport]
public class TimeoutUnit
{
public int Timeout { get; set; } = 0;
}
21 changes: 21 additions & 0 deletions sample/Library/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Net.Http;
using System.Runtime.InteropServices.JavaScript;

namespace Client.Library;

public partial class Program
{
public static void Main(string[] args)
{
Console.WriteLine(".NET Main method entered.");

// You can put any startup logic here like any other .NET application
// alternatively you could expose a class that embodies your app and treat the .NET code as a library.
// For this demo we'll go with the latter, PeopleApp will be constructed from the JS side.
Console.WriteLine($"{nameof(PeopleApp)} will be constructed from the JS side in this demo.");
}
}
12 changes: 12 additions & 0 deletions sample/Library/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"Client.Library": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:51663;http://localhost:51664"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;

namespace TypeShim.Sample;
namespace Client.Library;

public class RandomEntityGenerator
{
Expand Down
42 changes: 42 additions & 0 deletions sample/Library/wwwroot/TypeShimProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createWasmRuntime, TypeShimInitializer } from '@client/wasm-exports';
import { ReactNode, useEffect, useState } from 'react';

export interface AppProviderProps {
children: ReactNode;
}

export function TypeShimProvider({ children }: AppProviderProps) {
const [runtime, setRuntime] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
async function load() {
try {
const runtimeInfo = await createWasmRuntime();
await TypeShimInitializer.initialize(runtimeInfo);
console.log("WASM Runtime initialized successfully.");
} catch (err: any) {
console.error("Error loading WASM runtime:", err);
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
load();

return () => { cancelled = true; console.log("CANCEL"); }; // cleanup
}, []);
return error
? (<div>Error: {error}</div>)
: loading
? (<div>Loading...</div>)
: (<>{children}</>);
}
1 change: 1 addition & 0 deletions sample/Library/wwwroot/_framework/dotnet.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dotnet: any;
3 changes: 3 additions & 0 deletions sample/Library/wwwroot/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './wasm-bootstrap'
export * from './TypeShimProvider'
export * from './typeshim'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@typeshim/wasm-exports",
"name": "@client/wasm-exports",
"version": "1.0.0",
"description": "",
"main": "main.ts",
Expand Down
18 changes: 18 additions & 0 deletions sample/Library/wwwroot/wasm-bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { dotnet } from './_framework/dotnet'

let runtime: any = null;
let runtimePromise: Promise<any> | null = null;
export async function createWasmRuntime(): Promise<any> {
console.log("Creating WASM runtime...");
if (runtimePromise) {
console.warn("WASM runtime is already started. Not creating a new instance.");
return runtimePromise;
} else {
runtimePromise = dotnet.create();
}
const runtimeInfo = await runtimePromise;
console.log("WASM runtime info:", runtimeInfo);
const { runMain } = runtimeInfo;
runMain();
return runtime = runtimeInfo;
};
36 changes: 36 additions & 0 deletions sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## TLDR;
To start the vite dev server run:
```
npm run build
npm run dev
```

To start the ASP.NET backend, either start the project from your favorite IDE or:
```
dotnet run --project Server
```


The app should be available on [http://localhost:5012](http://localhost:5012)

# .NET WebAssembly + React with TypeShim

This sample bundles a .NET library into a react app, while TypeShim provides the interop boundary code. This is just _a_ possible way of using TypeShim and .NET Browser Apps.

The domain of this app is 'people', each person may have a pet (I had to come up with _something_). The classes representing this domain are defined in the `Library` project and consumed in the react `app` project for the UI. The same classes are also consumed in the `Server` project to facilitate an Api endpoint for pulling some generated data.

> A reasonable use case for .NET browser apps that is worth highlighting is that of the `PeopleApiClient` which defines how to interact with the `PeopleController` in the server project. As the Controller and ApiClient can share code, this requires no duplicate definitions, which is typically the case with JS+.NET mixed stacks.

#### Library
This project is where most 'logic' resides. This is rather simple stuff with classes like `Person`, `Pet` and `PeopleProvider` which are all part of the interop API (i.e. have `[TSExport]`).

When built, this project outputs an npm project in the `/Library/bin/wwwroot` directory which contains a combination of files from:
- The source code `Library/wwwroot`
- Dotnet wasm build artifacts
- And ofcourse: `typeshim.ts`, the generated TypeScript library that you are hopefully here to check out.

#### Server
This is an ASP.NET project that hosts the `PeopleController` and includes a proxy to expose the vite dev server from the `app` project. This provides generated test data for the app to consume.

#### app
This is the react app that consumes the .NET Library. The usage of interop classes generated by TypeShim can be found for example in `/app/src/people/PeopleRepository.tsx`.
8 changes: 2 additions & 6 deletions sample/Sample.slnx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
<Solution>
<Project Path="TypeShim.Sample.Server/TypeShim.Sample.Server.csproj" />
<Project Path="TypeShim.Sample/TypeShim.Sample.csproj" />
<Project Path="TypeShim.Sample.Client/TypeShim.Sample.Client.esproj">
<Build />
<Deploy />
</Project>
<Project Path="Server/Server.csproj" />
<Project Path="Library/Library.csproj" />
</Solution>
22 changes: 22 additions & 0 deletions sample/Server/Controllers/PeopleController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using Client.Library;

namespace Server.Controllers;

[ApiController]
[Route("[controller]")]
public class PeopleController() : ControllerBase
{
private readonly List<Person> _people = new RandomEntityGenerator().GeneratePersons(250);

[HttpGet]
[Route("all")]
public PeopleDto GetAll()
{
return new PeopleDto
{
People = [.. _people.Select(PersonDto.FromPerson)]
};
}
}

Loading
Loading