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
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@

- Keep the code lean and efficient, including the use of `unsafe` when it is justified.
- Use the latest available .NET and C# features when they improve the code and fit the existing style.
- Avoid defensive programming and compatibility overhead. Target only the modern 32-bit WASM runtime, current JS specs, and current browser capabilities.
- Follow the existing code style, architecture, project structure, naming, and formatting strictly.
- Do not stop at analysis or a partial fix. If the task requires code or verification, carry it through to the expected result.
- Avoid defensive programming and compatibility overhead. Target only the modern WASM runtime, current JS specs and current browser capabilities.
- Follow the existing code style, architecture, project structure, naming and formatting strictly.
- If clarification is required, use the question tool instead of guessing.

IMPORTANT: NEVER RUN ANY BUILD/PUBLISH COMMANDS IN PARALLEL.

# Packaging Bootsharp

Follow these steps exactly and sequentially whenever the Bootsharp package consumed by other projects must be actualized, or when running the JS end-to-end tests after updating JS or C# code.
Follow these steps exactly and sequentially whenever the Bootsharp package consumed by other projects must be actualized, or when running the JS end-to-end tests after updating the package's C# or JS code.

1. Build the JS package with `npm run build` under `src/js`.
2. Bump the Bootsharp library alpha version in `src/cs/Directory.Build.props`
Expand All @@ -23,13 +22,15 @@ Follow these steps exactly and sequentially whenever the Bootsharp package consu

Important: Always execute these steps in order, do not parallelize them.

Note: Bumping the package version is only required after modifying the package's C# or JS sources. If you're only editing E2E tests, there is no need to follow the full repackaging procedure each time.

# Code Coverage

We have a strict 100% coverage policy for both the C# and JS codebases.

- Tests must be meaningful and cover real behavior.
- Do not add fake tests just to satisfy the numbers.
- No unreachable code is allowed, except in rare cases where testing is not practical. In those cases, `[ExcludeFromCodeCoverage]` may be used deliberately.
- No unreachable code is allowed, except in rare cases where testing is not practical. In those cases, ask how to proceed.
- Treat branch coverage as part of the requirement, not just line coverage.

To check C# coverage, use `reportgenerator` on merged coverlet output. Example workflow reference: `src/cs/.scripts/cover.sh`. Do not run that script verbatim in automation; it is intended for interactive usage.
Expand Down
46 changes: 36 additions & 10 deletions docs/guide/declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Bootsharp will automatically generate [type declarations](https://www.typescript

For the interop methods, function declarations are emitted.

Exported `[JSInvokable]` methods will have associated function assigned under the declaring type space:
Exported methods will have associated function assigned under the declaring type space:

```csharp
public class Foo
{
[JSInvokable]
[Export]
public static void Bar() { }
}
```
Expand All @@ -32,14 +32,14 @@ import { Foo } from "bootsharp";
Foo.bar();
```

Imported `[JSFunction]` methods will be emitted as properties, which have to be assigned before booting the runtime:
Imported methods will be emitted as properties, which have to be assigned before booting the runtime:

::: code-group

```csharp [Foo.cs]
public partial class Foo
{
[JSFunction]
[Import]
public static partial void Bar();
}
```
Expand All @@ -60,21 +60,21 @@ Foo.bar = () => {};

## Event Declarations

`[JSEvent]` methods will be emitted as objects with `subscribe` and `unsubscribe` methods:
Exported events are emitted as `EventSubscriber` objects:

::: code-group

```csharp [Foo.cs]
public class Foo
{
[JSEvent]
public static partial void OnBar (string payload);
[Export]
public static event Action<string>? OnBar;
}
```

```ts [bindings.d.ts]
export namespace Foo {
export const onBar: Event<[string]>;
export const onBar: EventSubscriber<[payload: string]>;
}
```

Expand All @@ -86,6 +86,32 @@ Foo.onBar.subscribe(payload => {});

:::

Imported events are emitted as `EventBroadcaster` objects:

::: code-group

```csharp [Foo.cs]
public static partial class Foo
{
[Import]
public static event Action<string>? OnBar;
}
```

```ts [bindings.d.ts]
export namespace Foo {
export const onBar: EventBroadcaster<[payload: string]>;
}
```

```ts [main.ts]
import { Foo } from "bootsharp";

Foo.onBar.broadcast("updated");
```

:::

## Documentation Declarations

When an inspected assembly has XML documentation generated, Bootsharp mirrors the matching documentation into the emitted TypeScript declarations.
Expand All @@ -100,7 +126,7 @@ public class MathApi
/// <param name="left">Left number.</param>
/// <param name="right">Right number.</param>
/// <returns>The sum.</returns>
[JSInvokable]
[Export]
public static int Add (int left, int right) => left + right;
}
```
Expand Down Expand Up @@ -146,7 +172,7 @@ public record Bar (Foo foo);

public partial class Foo
{
[JSFunction]
[Import]
public static partial Bar GetBar();
}
```
Expand Down
8 changes: 2 additions & 6 deletions docs/guide/emit-prefs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Emit Preferences

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.
Use `[Preferences]` 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.

## Space

Expand All @@ -9,7 +9,7 @@ By default, all the generated JavaScript binding objects and TypeScript declarat
To customize emitted spaces, use `Space` parameter. For example, to make all bindings declared under "Foo.Bar" C# namespace have "Baz" namespace in JavaScript:

```cs
[assembly: JSPreferences(
[assembly: Preferences(
Space = ["^Foo\.Bar\.(\S+)", "Baz.$1"]
)]
```
Expand All @@ -24,10 +24,6 @@ The patterns are matched against full type name of declaring C# type when genera

Allows customizing generated TypeScript type syntax. The patterns are matched against full C# type names of interop method arguments, return values and object properties.

## Event

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...".

## Function

Customizes generated JavaScript function names. The patterns are matched against C# interop method names.
30 changes: 23 additions & 7 deletions docs/guide/events.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Events

To make a C# method act as event broadcaster for JavaScript consumers, annotate it with `[JSEvent]` attribute:
To expose a C# event to JavaScript consumers, declare a static event and annotate it with `[Export]`:

```csharp
[JSEvent]
public static partial void OnSomethingChanged (string payload);
[Export]
public static event Action<string>? OnSomethingChanged;

public static void UpdateSomething (string payload)
{
OnSomethingChanged?.Invoke(payload);
}
```

— and consume it from JavaScript as follows:
Expand All @@ -18,13 +23,24 @@ function handleSomething(payload: string) {
}
```

When the method in invoked in C#, subscribed JavaScript handlers will be notified. In TypeScript the event will have typed generic declaration corresponding to the event arguments.
When the event is raised in C#, subscribed JavaScript handlers will be notified. In TypeScript exported events are declared as `EventSubscriber<...>` with argument types inferred from the event delegate signature.

To use a JavaScript event from C#, declare a static event on a partial type and annotate it with `[Import]`:

## Events in Interop Interfaces
```csharp
[Import]
public static event Action<string>? OnSomethingChanged;
```

JavaScript will see it as an `EventBroadcaster`:

```ts
Program.onSomethingChanged.broadcast("updated");
```

To make a method in an [interop interface](/guide/interop-interfaces) act as event broadcaster, make its name start with "Notify". Such methods will be detected by Bootsharp and exposed to JavaScript as events with "Notify" changed to "On". For example, `NotifyUserUpdated` C# method will be exposed as `OnUserUpdated` JavaScript event.
Bootsharp supports all common event types: `Action`, `EventHandler`, and any custom delegate types without a return type.

Which interface methods are considered events and the way they are named in JavaScript can be customized with [emit preferences](/guide/emit-prefs).
Events on the [interop interfaces](/guide/interop-interfaces) are picked up automatically, so you don't have to annotate them.

## React Event Hooks

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/extensions/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ using Bootsharp;
using Bootsharp.Inject;
using Microsoft.Extensions.DependencyInjection;

[assembly: JSExport(
[assembly: Export(
typeof(IExported),
// other APIs to export to JavaScript
)]

[assembly: JSImport(
[assembly: Import(
typeof(IImported),
// other APIs to import from JavaScript
)]
Expand Down
12 changes: 6 additions & 6 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ using Bootsharp;

public static partial class Program
{
[Export] // Used in JS as Program.onMainInvoked.subscribe(..)
public static event Action<string>? OnMainInvoked;

public static void Main ()
{
OnMainInvoked($"Hello {GetFrontendName()}, .NET here!");
OnMainInvoked?.Invoke($"Hello {GetFrontendName()}, .NET here!");
}

[JSEvent] // Used in JS as Program.onMainInvoked.subscribe(..)
public static partial void OnMainInvoked (string message);

[JSFunction] // Set in JS as Program.getFrontendName = () => ..
[Import] // Set in JS as Program.getFrontendName = () => ..
public static partial string GetFrontendName ();

[JSInvokable] // Invoked from JS as Program.GetBackendName()
[Export] // Invoked from JS as Program.GetBackendName()
public static string GetBackendName () => Environment.Version;
}
```
Expand Down
5 changes: 2 additions & 3 deletions docs/guide/interop-instances.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ public class Exported : IExported

public static partial class Factory
{
[JSInvokable] public static IExported GetExported () => new Exported();
[JSFunction] public static partial IImported GetImported ();
[Export] public static IExported GetExported () => new Exported();
[Import] public static partial IImported GetImported ();
}

var imported = Factory.GetImported();
Expand All @@ -51,5 +51,4 @@ _ = exported.value; // invokes the C# getter

Interop instances are subject to the following limitations:
- Can't be args or return values of other interop instance method
- Can't be args of events
- Interfaces from "System" namespace are not qualified
22 changes: 10 additions & 12 deletions docs/guide/interop-interfaces.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,36 @@
# Interop Interfaces

Instead of manually authoring a binding for each method, let Bootsharp generate them automatically using the `[JSImport]` and `[JSExport]` assembly attributes.
Instead of manually authoring a binding for each member, let Bootsharp generate them automatically using the `[Import]` and `[Export]` assembly attributes.

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:
For example, say we have a JavaScript UI (frontend) with a setting stored on the JS side, and a C# domain layer (backend) that wants to expose state changes back to JavaScript. You can describe the imported frontend APIs like this:

```csharp
interface IFrontend
{
bool IsMuted { get; set; }
void NotifyDataChanged (Data data);
}
```

Now, add the interface type to the JS import list:

```csharp
[assembly: JSImport([
typeof(IFrontend)
])]
[assembly: Import(typeof(IFrontend))]
```

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:

```ts
export namespace Frontend {
export let isMuted: boolean;
export const onDataChanged: Event<[Data]>;
}
```

Now, say we want to provide an API for the frontend to request a mutation of the data:
Now, export the backend contract to JavaScript:

```csharp
interface IBackend
{
event Action<Data> OnDataChanged;
Data? Current { get; set; }
void AddData (Data data);
}
Expand All @@ -42,20 +39,21 @@ interface IBackend
Export the interface to JavaScript:

```csharp
[assembly: JSExport([
typeof(IBackend)
])]
[assembly: Export(typeof(IBackend))]
```

This will produce the following spec to be consumed on the JavaScript side:

```ts
export namespace Backend {
export let current: Data | null;
export const onDataChanged: EventSubscriber<[data: Data]>;
export let current: Data | undefined;
export function addData(data: Data): void;
}
```

Imported interface events work the other way around: declare a real C# event on the interface, and Bootsharp will generate a JavaScript `EventBroadcaster` plus a regular subscribable event on the generated C# implementation.

To make Bootsharp automatically inject and initialize the generated interop implementations, use the [dependency injection](/guide/extensions/dependency-injection) extension.

::: tip Example
Expand Down
16 changes: 8 additions & 8 deletions docs/guide/namespaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Bootsharp maps generated binding APIs based on the name of the associated C# typ
Full type name (including namespace) of the declaring type of the static interop method is mapped into JavaScript object name:

```csharp
class Class { [JSInvokable] static void Method() {} }
namespace Foo { class Class { [JSInvokable] static void Method() {} } }
namespace Foo.Bar { class Class { [JSInvokable] static void Method() {} } }
class Class { [Export] static void Method() {} }
namespace Foo { class Class { [Export] static void Method() {} } }
namespace Foo.Bar { class Class { [Export] static void Method() {} } }
```

```ts
Expand All @@ -27,7 +27,7 @@ namespace Foo;

public class Class
{
public class Nested { [JSInvokable] public static void Method() {} }
public class Nested { [Export] public static void Method() {} }
}
```

Expand All @@ -42,11 +42,11 @@ Foo.Class.Nested.method();
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.

```csharp
[JSExport([
[Export(
typeof(IExported),
typeof(Foo.IExported),
typeof(Foo.Bar.IExported),
])]
typeof(Foo.Bar.IExported)
)]

interface IExported { void Method(); }
namespace Foo { interface IExported { void Method(); } }
Expand All @@ -71,7 +71,7 @@ namespace Foo { public record Record; }

partial class Class
{
[JSFunction]
[Import]
public static partial Record Method(Foo.Record r);
}
```
Expand Down
Loading
Loading