Skip to content

Commit

Permalink
iteration
Browse files Browse the repository at this point in the history
  • Loading branch information
elringus committed Sep 7, 2023
1 parent 6c1f917 commit 47ae1a8
Show file tree
Hide file tree
Showing 21 changed files with 101 additions and 54 deletions.
7 changes: 7 additions & 0 deletions DotNet/Bootsharp.Builder.Test/DeclarationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ public class DeclarationTest : ContentTest
{
protected override string TestedContent => GeneratedDeclarations;

[Fact]
public void ImportsEventType ()
{
Task.Execute();
Contains("""import type { Event } from "./event";""");
}

[Fact]
public void DeclaresNamespace ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal sealed class DeclarationGenerator(NamespaceBuilder spaceBuilder)
private readonly TypeDeclarationGenerator typesGenerator = new(spaceBuilder);

public string Generate (AssemblyInspector inspector) => JoinLines(0,
"""import type { Event } from "./event";""",
typesGenerator.Generate(inspector.Types),
methodsGenerator.Generate(inspector.Methods)
) + "\n";
Expand Down
35 changes: 30 additions & 5 deletions DotNet/Bootsharp.Generator.Test/Emitters/ImportTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,32 @@ internal static void RegisterDynamicDependencies () { }
}
"""
},
// Can detect event methods.
// Will detect and override event methods with defaults.
new object[] {
"""
[assembly:JSImport(typeof(IFoo))]

public interface IFoo
{
void NotifyFoo (string foo);
}
""",
"""
namespace Foo;

public class JSFoo : global::IFoo
{
[ModuleInitializer]
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "Foo.JSFoo", "GeneratorTest")]
internal static void RegisterDynamicDependencies () { }

[JSEvent] public static void OnFoo (global::System.String foo) => Event.Broadcast("Foo.onFoo", foo);

void global::IFoo.NotifyFoo (global::System.String foo) => OnFoo(foo);
}
"""
},
// Can detect but not override event methods.
new object[] {
"""
[assembly:JSImport(typeof(IFoo), EventPattern=@"(^Notify)(\S+)", EventReplacement=null)]
Expand Down Expand Up @@ -67,11 +92,11 @@ internal static void RegisterDynamicDependencies () { }
// Can detect and override event methods.
new object[] {
"""
[assembly:JSImport(typeof(IFoo), EventPattern=@"(^Notify)(\S+)", EventReplacement="On$2")]
[assembly:JSImport(typeof(IFoo), EventPattern=@"(^Fire)(\S+)", EventReplacement="Handle$2")]

public interface IFoo
{
void NotifyFoo (string foo);
void FireFoo (string foo);
}
""",
"""
Expand All @@ -83,9 +108,9 @@ public class JSFoo : global::IFoo
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "Foo.JSFoo", "GeneratorTest")]
internal static void RegisterDynamicDependencies () { }

[JSEvent] public static void OnFoo (global::System.String foo) => Event.Broadcast("Foo.onFoo", foo);
[JSEvent] public static void HandleFoo (global::System.String foo) => Event.Broadcast("Foo.handleFoo", foo);

void global::IFoo.NotifyFoo (global::System.String foo) => OnFoo(foo);
void global::IFoo.FireFoo (global::System.String foo) => HandleFoo(foo);
}
"""
},
Expand Down
16 changes: 13 additions & 3 deletions DotNet/Bootsharp.Generator/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal static class Common
public const string InvokeReplacementArg = "InvokeReplacement";
public const string EventPatternArg = "EventPattern";
public const string EventPatternReplacementArg = "EventReplacement";
public const string DefaultEventPattern = @"(^Notify)(\S+)";
public const string DefaultEventReplacement = "On$2";

public static string EmitCommon (string source)
=> $"""
Expand Down Expand Up @@ -95,14 +97,22 @@ public static string ConvertMethodName (string name, AttributeData attribute)

public static bool IsEvent (string name, AttributeData attribute)
{
var pattern = attribute.NamedArguments.FirstOrDefault(a => a.Key == EventPatternArg).Value.Value as string;
var pattern = default(string);
if (!attribute.NamedArguments.Any(a => a.Key == EventPatternArg)) pattern = DefaultEventPattern;
else pattern = attribute.NamedArguments.First(a => a.Key == EventPatternArg).Value.Value as string;
return !string.IsNullOrEmpty(pattern) && Regex.IsMatch(name, pattern);
}

public static string ConvertEventName (string name, AttributeData attribute)
{
var pattern = attribute.NamedArguments.FirstOrDefault(a => a.Key == EventPatternArg).Value.Value as string;
var replacement = attribute.NamedArguments.FirstOrDefault(a => a.Key == EventPatternReplacementArg).Value.Value as string;
var pattern = default(string);
if (!attribute.NamedArguments.Any(a => a.Key == EventPatternArg)) pattern = DefaultEventPattern;
else pattern = attribute.NamedArguments.First(a => a.Key == EventPatternArg).Value.Value as string;

var replacement = default(string);
if (!attribute.NamedArguments.Any(a => a.Key == EventPatternReplacementArg)) replacement = DefaultEventReplacement;
else replacement = attribute.NamedArguments.First(a => a.Key == EventPatternReplacementArg).Value.Value as string;

if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(replacement)) return name;
return Regex.Replace(name, pattern, replacement);
}
Expand Down
4 changes: 1 addition & 3 deletions DotNet/Bootsharp.Generator/Emitters/ExportType.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Bootsharp.Generator;

Expand All @@ -20,12 +19,11 @@ public string EmitSource (Compilation compilation)
var specType = BuildFullName(type);
var implType = BuildBindingType(type);
var space = BuildBindingNamespace(type);
var modifiers = (type.DeclaringSyntaxReferences[0].GetSyntax() as InterfaceDeclarationSyntax)!.Modifiers;
return EmitCommon
($$"""
namespace {{space}};

{{modifiers}} class {{implType}}
public class {{implType}}
{
private static {{specType}} handler = null!;

Expand Down
6 changes: 3 additions & 3 deletions DotNet/Bootsharp.Test/JSTypesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public void NameAndInvokeParametersAreNullByDefault ()
}

[Fact]
public void EventParametersAreNotNullByDefault ()
public void EventParametersAreNullByDefault () // (defaults are in generator)
{
var attribute = new JSImportAttribute(typeof(IBackend));
Assert.Equal(@"(^Notify)(\S+)", attribute.EventPattern);
Assert.Equal("On$2", attribute.EventReplacement);
Assert.Null(attribute.EventPattern);
Assert.Null(attribute.EventReplacement);
}

[Fact]
Expand Down
6 changes: 3 additions & 3 deletions DotNet/Bootsharp/Attributes/JSExportAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
/// </summary>
/// <remarks>
/// Generated bindings have to be initialized with the handler implementation.
/// For example, given 'IHandler' interface is exported, 'JSHandler' class will be generated,
/// which has to be instantiated with an 'IHandler' implementation instance.
/// For example, given "IHandler" interface is exported, "JSHandler" class will be generated,
/// which has to be instantiated with an "IHandler" implementation instance.
/// </remarks>
/// <example>
/// Expose 'IHandlerA' and 'IHandlerB' C# APIs to JavaScript and wrap invocations in 'Utils.Try()':
/// Expose "IHandlerA" and "IHandlerB" C# APIs to JavaScript and wrap invocations in "Utils.Try()":
/// <code>
/// [assembly: JSExport(
/// typeof(IHandlerA),
Expand Down
2 changes: 1 addition & 1 deletion DotNet/Bootsharp/Attributes/JSFunctionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// Applied to a partial method to bind it with a JavaScript function.
/// </summary>
/// <remarks>
/// The implementation is expected to be assigned as 'Namespace.method = function'.
/// The implementation is expected to be assigned as "Namespace.method = function".
/// </remarks>
/// <example>
/// <code>
Expand Down
12 changes: 6 additions & 6 deletions DotNet/Bootsharp/Attributes/JSImportAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
/// </summary>
/// <remarks>
/// Generated bindings have to be implemented on JavaScript side.
/// For example, given 'IFrontend' interface is imported, 'JSFrontend' class will be generated,
/// For example, given "IFrontend" interface is imported, "JSFrontend" class will be generated,
/// which has to be implemented in JavaScript.<br/>
/// When an interface method starts with 'Notify', an event bindings will ge generated (instead of function);
/// JavaScript name of the event will start with 'on' instead of 'Notify'. This behaviour can be configured
/// When an interface method starts with "Notify", an event bindings will ge generated (instead of function);
/// JavaScript name of the event will start with "on" instead of "Notify". This behaviour can be configured
/// with <see cref="EventPattern"/> and <see cref="EventReplacement"/> parameters.
/// </remarks>
/// <example>
/// Generate JavaScript APIs based on 'IFrontendAPI' and 'IOtherFrontendAPI' interfaces:
/// Generate JavaScript APIs based on "IFrontendAPI" and "IOtherFrontendAPI" interfaces:
/// <code>
/// [assembly: JSImport(
/// typeof(IFrontendAPI),
Expand All @@ -27,11 +27,11 @@ public sealed class JSImportAttribute : JSTypeAttribute
/// <summary>
/// Regex pattern to match method names indicating an event binding should generated (instead of function).
/// </summary>
public string? EventPattern { get; init; } = @"(^Notify)(\S+)";
public string? EventPattern { get; init; }
/// <summary>
/// Replacement for the event pattern matches.
/// </summary>
public string? EventReplacement { get; init; } = "On$2";
public string? EventReplacement { get; init; }

/// <inheritdoc/>
public JSImportAttribute (params Type[] types)
Expand Down
2 changes: 1 addition & 1 deletion DotNet/Bootsharp/Attributes/JSNamespaceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// generated for JavaScript bindings and type definitions.
/// </summary>
/// <example>
/// Transform 'Company.Product.Space' into 'Space':
/// Transform "Company.Product.Space" into "Space":
/// <code>[assembly:JSNamespace(@"Company\.Product\.(\S+)", "$1")]</code>
/// </example>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
Expand Down
2 changes: 1 addition & 1 deletion DotNet/Bootsharp/Bootsharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<AssemblyTitle>Bootsharp</AssemblyTitle>
<Title>Bootsharp</Title>
<PackageId>Bootsharp</PackageId>
<Version>0.0.2-alpha.34</Version>
<Version>0.0.2-alpha.41</Version>
<Authors>Elringus</Authors>
<Description>Compile C# solution into single-file ES module with auto-generated JavaScript bindings and type definitions.</Description>
<PackageTags>javascript typescript ts js wasm bindings interop codegen</PackageTags>
Expand Down
4 changes: 2 additions & 2 deletions Samples/App/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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), AOT-compiling, customizing various Bootsharp build options, side-loading binaries, mocking C# APIs in frontend unit tests and using type definitions.
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), AOT-compiling, customizing various Bootsharp build options, side-loading binaries, mocking C# APIs in frontend unit tests, using events and type definitions.

How to test:
- Run `npm run test` to run frontend unit tests;
- Run `npm run backend` to complile C# backend;
- Run `npm run test` to run frontend unit tests;
- Run `npm run dev` to run local dev server;
- Run `npm run build` to build the app.
16 changes: 16 additions & 0 deletions Samples/App/backend/Backend.Prime/IPrimeComputerUI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Backend.Prime;

// Contract of the prime computer user interface.
// The implementation goes to the frontend,
// so that backend is not coupled with the details.

public interface IPrimeComputerUI
{
int GetComplexity ();

// Imported methods starting with "Notify" will automatically
// be converted to JavaScript events and renamed to "On...".
// This can be configured with "JSImport.EventPattern" and
// "JSImport.EventReplacement" attribute parameters.
void NotifyComplete (int time);
}
2 changes: 1 addition & 1 deletion Samples/App/backend/Backend.Prime/PrimeComputer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Implementation of the computer service that compute prime numbers.
// Injected in the application entry point assembly (Backend.WASM).

public class PrimeComputer(IComputerUI ui) : IComputer
public class PrimeComputer(IPrimeComputerUI ui) : IComputer
{
private CancellationTokenSource? cts;

Expand Down
2 changes: 1 addition & 1 deletion Samples/App/backend/Backend.WASM/Backend.WASM.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0-rc.2.23431.9"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0-rc.2.23456.8"/>
<PackageReference Include="Bootsharp" Version="*-*"/>
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion Samples/App/backend/Backend.WASM/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

// Auto-generate JavaScript interop handlers for specified contracts.
[assembly: JSExport(typeof(IComputer))]
[assembly: JSImport(typeof(IComputerUI))]
[assembly: JSImport(typeof(IPrimeComputerUI))]

// Perform dependency injection.
new ServiceCollection()
Expand Down
2 changes: 1 addition & 1 deletion Samples/App/backend/Backend/IComputer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Backend;

// In the domain assembly we outline the contract of a computer service.
// The specific implementations are in other assemblies, so that
// The specific implementation is in other assembly, so that
// domain is not coupled with the details.

public interface IComputer
Expand Down
11 changes: 0 additions & 11 deletions Samples/App/backend/Backend/IComputerUI.cs

This file was deleted.

2 changes: 1 addition & 1 deletion Samples/App/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"scripts": {
"test": "",
"backend": "dotnet publish backend",
"test": "",
"dev": "vite",
"preview": "vite preview",
"build": "tsc && vite build"
Expand Down
17 changes: 9 additions & 8 deletions Samples/App/src/computer.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { useEffect, useState, useCallback } from "react";
import { Backend, Frontend } from "backend";
import { Computer, PrimeComputerUI } from "backend";

type Props = {
complexity: number;
resultLimit: number;
};

export default ({ complexity }: Props) => {
export default ({ complexity, resultLimit }: Props) => {
const [computing, setComputing] = useState(false);
const [results, setResults] = useState("");

const toggle = useCallback(() => {
if (Backend.isComputing()) Backend.stopComputing();
else Backend.startComputing();
if (Computer.isComputing()) Computer.stopComputing();
else Computer.startComputing();
setComputing(!computing);
}, [computing]);

const logResult = useCallback((time: number) => {
setResults(i => {
if (i.length > 999) i = i.substring(0, i.lastIndexOf("\n"));
if (i.length > resultLimit) i = i.substring(0, i.lastIndexOf("\n"));
const stamp = new Date().toLocaleTimeString([], { hour12: false });
return `[${stamp}] Computed in ${time}ms.\n${i}`;
});
}, []);

useEffect(() => {
Frontend.getComplexity = () => complexity;
PrimeComputerUI.getComplexity = () => complexity;
}, [complexity]);

useEffect(() => {
Frontend.onComplete.subscribe(logResult);
return () => Frontend.onComplete.unsubscribe(logResult);
PrimeComputerUI.onComplete.subscribe(logResult);
return () => PrimeComputerUI.onComplete.unsubscribe(logResult);
}, [logResult]);

return (
Expand Down
4 changes: 2 additions & 2 deletions Samples/App/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ import "index.css";
await bootBackend();

createRoot(document.getElementById("app")!).render([
createElement(require("donut"), 33333),
createElement(require("computer"), 60)
createElement(require("donut"), { fps: 60 }),
createElement(require("computer"), { complexity: 33333, resultLimit: 999 })
]);

0 comments on commit 47ae1a8

Please sign in to comment.