From 5e2d01dc939447aa944967914c35c9718ca47e67 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 11:25:44 +0300 Subject: [PATCH 1/9] use jsname --- .../Common/Inspection/OverloadDisambiguator.cs | 8 ++++---- .../Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs index 75a97e70..48f1a9bd 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs @@ -24,10 +24,10 @@ private static void Disambiguate (SurfaceMeta srf, IReadOnlyList ove private static Dictionary MapArgs (IReadOnlyList overloaded) { var baseline = ResolveBaseline(overloaded); // baseline is the method that won't be renamed - var baselineArgs = baseline.Args.Select(a => a.Name).ToHashSet(); + var baselineArgs = baseline.Args.Select(a => a.JSName).ToHashSet(); return overloaded.Where(m => m != baseline).ToDictionary(m => m, m => m.Args - .Where(a => !baselineArgs.Contains(a.Name)) - .Select(a => ToFirstUpper(a.Name)) + .Where(a => !baselineArgs.Contains(a.JSName)) + .Select(a => ToFirstUpper(a.JSName)) // if an overload has extra args — use their names as discriminator .ToArray() is { Length: > 0 } extra ? extra : GetArgNames(m)); } @@ -38,7 +38,7 @@ private static IEnumerable FindAmbiguous (Dictionary g.Select(kv => kv.Key)); private static string[] GetArgNames (MethodMeta method) => method.Args - .Select(a => ToFirstUpper(a.Name)).ToArray(); + .Select(a => ToFirstUpper(a.JSName)).ToArray(); private static string[] GetArgTypes (MethodMeta method) => method.Args .Select(a => a.Value.Type.Clr.Name).ToArray(); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs index 00aa7f15..190f0fe2 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs @@ -76,7 +76,7 @@ private string EmitEvent (EventMeta evt) private string EmitMethod (MethodMeta method) { - var sigArgs = string.Join(", ", method.Args.Select(a => a.Name)); + var sigArgs = string.Join(", ", method.Args.Select(a => a.JSName)); var invArgs = sigArgs.Length > 0 ? $"this._id, {sigArgs}" : "this._id"; var bodyExp = $"{md.Ref(method.Surf)}.{method.JSName}({invArgs})"; if (!method.Void) bodyExp = $"return {bodyExp}"; From 0432abe268a3132e2190b7bf298f07e285dc5d05 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 11:51:02 +0300 Subject: [PATCH 2/9] escape reserved --- .../GenerateCS/InteropTest.cs | 8 +++++ .../GenerateJS/DeclarationTest.cs | 4 +-- .../GenerateJS/JSModuleTest.cs | 6 ++-- .../Common/Global/GlobalInspection.cs | 33 +++++++++++++++++++ .../Common/Global/GlobalType.cs | 14 ++++---- .../Common/Inspection/TypeInspector.cs | 8 ++--- src/cs/Directory.Build.props | 2 +- 7 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs index 1b4daf7e..a5051876 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs @@ -443,6 +443,14 @@ public partial class Class Contains("public static async global::System.Threading.Tasks.Task Space_Class_FunB (global::Space.Record?[]? a) => Serializer.Deserialize(await Space_Class_FunB_Serialized(Serializer.Serialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); } + [Fact] + public void EscapesReservedArgumentNames () + { + AddAssembly(WithClass("[Export] public static void Foo (string @object, int @class) {}")); + Execute(); + Contains("[JSExport] internal static void Class_Foo (global::System.String @object, global::System.Int32 @class) => global::Class.Foo(@object, @class);"); + } + [Fact] public void RespectsSpacePref () { diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index 08675398..8b82c698 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -1480,13 +1480,13 @@ public partial class Class [Import] public static event EventHandler? HandlerEvt; /// Runs foo. - /// Function value. + /// Function value. /// Names to run. /// /// Computed value to be used with and , /// or when invalid. /// - [Export] public static int Foo (List function, string[] names) => 0; + [Export] public static int Foo (List fn, string[] names) => 0; /// Gets payload. [Export] public static Payload Get (Kind kind) => default; diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index fb65c9dd..d655ed78 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -281,14 +281,14 @@ public void WhenNoSpaceBindingsAreAssignedToRootModule () } [Fact] - public void VariablesConflictingWithJSTypesAreRenamed () + public void EscapesReservedArgumentNames () { - AddAssembly(WithClass("[Export] public static void Fun (string function) {}")); + AddAssembly(WithClass("[Export] public static void Foo (string Class, int Function) {}")); Execute(); Contains( """ export const Class = { - fun: (fn) => exports.Class_Fun(fn) + foo: ($class, $function) => exports.Class_Foo($class, $function) }; """); } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 54278e28..3dbb5edc 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -10,6 +10,28 @@ internal static class GlobalInspection { public static Preferences Pref => PreferencesResolver.Resolved.Value!; + private static readonly HashSet csKeywords = [ + "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", + "checked", "class", "const", "continue", "decimal", "default", "delegate", + "do", "double", "else", "enum", "event", "explicit", "extern", "false", + "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", + "in", "int", "interface", "internal", "is", "lock", "long", "namespace", + "new", "null", "object", "operator", "out", "override", "params", "private", + "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", + "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", + "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", + "unsafe", "ushort", "using", "virtual", "void", "volatile", "while" + ]; + + private static readonly HashSet jsKeywords = [ + "await", "break", "case", "catch", "class", "const", "continue", "debugger", + "default", "delete", "do", "else", "enum", "export", "extends", "false", + "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", + "protected", "public", "return", "static", "super", "switch", "this", + "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield" + ]; + public static MetadataLoadContext CreateLoadContext (string directory) { var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); @@ -41,6 +63,17 @@ public static bool IsAutoProperty (PropertyInfo prop) return backingField != null; } + public static string BuildCSName (string name) + { + return csKeywords.Contains(name) ? $"@{name}" : name; + } + + public static string BuildJSName (string name) + { + name = ToFirstLower(name); + return jsKeywords.Contains(name) ? $"${name}" : name; + } + public static string WithPref (IReadOnlyCollection prefs, string input, string? @default = null) { foreach (var pref in prefs) diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 80394ed0..9bd49c5e 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -24,6 +24,14 @@ public static bool IsTaskLike (Type type) return type.GetMethod(nameof(Task.GetAwaiter)) != null; } + public static bool IsDelegate (Type type) + { + for (var bs = type.BaseType; bs != null; bs = bs.BaseType) + if (bs.FullName == "System.MulticastDelegate") + return true; + return false; + } + public static bool IsTaskWithResult (Type type, [NotNullWhen(true)] out Type? result) { return (result = IsTaskLike(type) && type.GenericTypeArguments.Length == 1 @@ -94,12 +102,6 @@ public static bool IsNullable (Type type, NullabilityInfo? info, [NotNullWhen(tr return value != null; } - public static string BuildJSName (string name) - { - name = ToFirstLower(name); - return name == "function" ? "fn" : name; - } - public static string PrependIdArg (string args) { if (string.IsNullOrEmpty(args)) return "_id"; diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index 038632f4..6299f5b8 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -127,7 +127,7 @@ static bool ShouldInspectMethod (MethodInfo method) private EventMeta InspectEvent (EventInfo evt, InteropKind ik, SurfaceMeta srf) => new(evt) { IK = ik, Surf = srf, - Name = evt.Name, + Name = BuildCSName(evt.Name), JSName = WithPref(Pref.Event, evt.Name, BuildJSName(evt.Name)), TypeSyntax = BuildSyntax(evt.EventHandlerType!, GetNullity(evt)), Args = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() @@ -137,7 +137,7 @@ static bool ShouldInspectMethod (MethodInfo method) private PropertyMeta InspectProperty (PropertyInfo prop, InteropKind ik, SurfaceMeta srf) => new(prop) { IK = ik, Surf = srf, - Name = prop.Name, + Name = BuildCSName(prop.Name), JSName = WithPref(Pref.Property, prop.Name, BuildJSName(prop.Name)), TypeSyntax = BuildSyntax(prop.PropertyType, GetNullity(prop)), Get = prop.GetMethod != null ? InspectValue(prop.PropertyType, GetNullity(prop), ik) : null, @@ -147,7 +147,7 @@ static bool ShouldInspectMethod (MethodInfo method) private MethodMeta InspectMethod (MethodInfo method, InteropKind ik, SurfaceMeta srf) => new(method) { IK = ik, Surf = srf, - Name = method.Name, + Name = BuildCSName(method.Name), JSName = WithPref(Pref.Method, method.Name, BuildJSName(method.Name)), Args = method.GetParameters().Select(p => InspectArg(p, GetNullity(p), ik.Invert)).ToArray(), Return = InspectValue(method.ReturnParameter.ParameterType, GetNullity(method.ReturnParameter), ik), @@ -156,7 +156,7 @@ static bool ShouldInspectMethod (MethodInfo method) }; private ArgumentMeta InspectArg (ParameterInfo param, NullabilityInfo nil, InteropKind ik) => new(param) { - Name = param.Name!, + Name = BuildCSName(param.Name!), JSName = BuildJSName(param.Name!), Value = InspectValue(param.ParameterType, nil, ik) }; diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 6d6ef416..e3d7d969 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.324 + 0.8.0-alpha.328 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 11b64537f4b5cd681f5ccf2dcd2afd336da5e970 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 12:22:04 +0300 Subject: [PATCH 3/9] iter --- .../Common/Inspection/Meta/SurfaceMeta.cs | 12 ------------ .../Common/Inspection/TypeInspector.cs | 11 ++++------- src/cs/Directory.Build.props | 2 +- src/js/test/cs/Test.Library/Registries/Registries.cs | 2 ++ 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs index 8bf23dfe..10ebe533 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs @@ -69,19 +69,7 @@ public record SurfaceProxy /// public required string Id { get; init; } /// - /// Namespace of the generated C# proxy type. - /// - public required string Space { get; init; } - /// - /// Type name of the generated C# proxy type. - /// - public required string Name { get; init; } - /// /// Fully qualified C# syntax of the generated C# proxy type. /// public required string Syntax { get; init; } - /// - /// Full object name of the generated proxy on the JavaScript side. - /// - public required string JS { get; init; } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index 6299f5b8..040d2772 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -85,6 +85,7 @@ public IReadOnlyCollection Collect () static bool IsInstanced (Type type) { + if (IsDelegate(type)) return true; // Instanced types are mutable user types that are passed by reference when crossing the // interop boundary (as opposed to serialized immutable types, which are copied by value). if (!IsUserType(type)) return false; @@ -176,13 +177,9 @@ private TypeMeta InspectType (Type type, InteropKind ik) private SurfaceProxy BuildProxy (Type type, InteropKind ik) { - var space = "Bootsharp.Generated." + (ik == InteropKind.Export ? "Exports" : "Imports"); - if (type.Namespace != null) space += $".{type.Namespace}"; - var name = "JS" + (type.IsInterface ? type.Name[1..] : type.Name); - var id = $"{space}.{name}".Replace(".", "_").Replace('+', '_'); - var stx = $"global::{space}.{name}"; - var js = type.Namespace == null ? name : $"{type.Namespace}.{name}".Replace(".", "_"); - return new SurfaceProxy { Id = id, Space = space, Name = name, Syntax = stx, JS = js }; + var id = "JS_" + (ik == InteropKind.Export ? "Export_" : "Import_") + BuildId(type); + var stx = $"global::Bootsharp.Generated.{id}"; + return new SurfaceProxy { Id = id, Syntax = stx }; } private InteropKind? ResolveIK (MemberInfo info) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index e3d7d969..64bba907 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.328 + 0.8.0-alpha.329 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test.Library/Registries/Registries.cs b/src/js/test/cs/Test.Library/Registries/Registries.cs index ecd09614..818fd1a8 100644 --- a/src/js/test/cs/Test.Library/Registries/Registries.cs +++ b/src/js/test/cs/Test.Library/Registries/Registries.cs @@ -8,6 +8,8 @@ namespace Test.Library; public partial class Registries { + [Export] public static Func GetFunc () => () => "XXX"; + [Export] public static event Action? OnVehicleBroadcast; public static IRegistryProvider Provider { get; set; } = null!; From a3bf2e6a680fd6e6d89f8cea42235cb44558367c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 12:41:45 +0300 Subject: [PATCH 4/9] fix proxies --- .../GenerateCS/InstancesTest.cs | 43 ++-- .../GenerateCS/InteropTest.cs | 52 ++--- .../GenerateCS/ModulesTest.cs | 183 ++++++++---------- .../GenerateJS/JSInstanceTest.cs | 24 +-- .../GenerateJS/JSModuleTest.cs | 24 +-- .../GenerateCS/InstanceGenerator.cs | 52 +++-- .../GenerateCS/ModuleGenerator.cs | 43 ++-- .../GenerateJS/JSInstanceGenerator.cs | 2 +- src/cs/Directory.Build.props | 2 +- .../cs/Test.Library/Registries/Registries.cs | 2 +- 10 files changed, 197 insertions(+), 230 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs index ae869405..3813e473 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs @@ -31,23 +31,20 @@ public class Class Execute(); Contains( """ - namespace Bootsharp.Generated.Imports + public class JS_Import_IImported (int id) : global::Bootsharp.JSProxy(id), global::IImported { - public class JSImported (int id) : global::Bootsharp.JSProxy(id), global::IImported + ~JS_Import_IImported() => Instances.DisposeImported(_id); + + public event global::System.Action OnRecordChanged; + internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); + public event global::IImported.SomethingChanged OnSomethingChanged; + internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); + global::Record? global::IImported.Record { - ~JSImported() => Instances.DisposeImported(_id); - - public event global::System.Action OnRecordChanged; - internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); - public event global::IImported.SomethingChanged OnSomethingChanged; - internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); - global::Record? global::IImported.Record - { - get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetRecord(_id); - set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetRecord(_id, value); - } - void global::IImported.Fun (global::System.String arg) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); + get => global::Bootsharp.Generated.Interop.JS_Import_IImported_GetRecord(_id); + set => global::Bootsharp.Generated.Interop.JS_Import_IImported_SetRecord(_id, value); } + void global::IImported.Fun (global::System.String arg) => global::Bootsharp.Generated.Interop.JS_Import_IImported_Fun(_id, arg); } """); } @@ -117,14 +114,14 @@ public partial class Class Execute(); Contains( """ - internal static int Export (global::IExported it) => Export(it, static (_id, it) => { - it.Changed += HandleChanged; - return () => { - it.Changed -= HandleChanged; - }; - - void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.IExported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); - }); + internal static int Export (global::IExported it) => Export(it, static (_id, it) => { + it.Changed += HandleChanged; + return () => { + it.Changed -= HandleChanged; + }; + + void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.IExported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); + }); """); } @@ -160,6 +157,6 @@ public interface IInstanced; public interface IModule { IInstanced Item { get; set; } } """)); Execute(); - Contains("public class JSInstanced (int id) : global::Bootsharp.JSProxy(id), global::IInstanced"); + Contains("public class JS_Import_IInstanced (int id) : global::Bootsharp.JSProxy(id), global::IInstanced"); } } diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs index a5051876..baecba11 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs @@ -154,9 +154,9 @@ public interface IExported { Info Inv (string str, Info info); } public interface IImported { Info Fun (string str, Info info); } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static long Bootsharp_Generated_Exports_Space_JSExported_Inv (global::System.String str, [JSMarshalAs] long info) => Serializer.Serialize(global::Bootsharp.Generated.Exports.Space.JSExported.Inv(str, Serializer.Deserialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); - Contains("""[JSImport("IImported.funSerialized", "space")] [return: JSMarshalAs] internal static partial long Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized (global::System.String str, [JSMarshalAs] long info);"""); - Contains("public static global::Space.Info Bootsharp_Generated_Imports_Space_JSImported_Fun (global::System.String str, global::Space.Info info) => Serializer.Deserialize(Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized(str, Serializer.Serialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); + Contains("[JSExport] [return: JSMarshalAs] internal static long JS_Export_Space_IExported_Inv (global::System.String str, [JSMarshalAs] long info) => Serializer.Serialize(global::Bootsharp.Generated.JS_Export_Space_IExported.Inv(str, Serializer.Deserialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); + Contains("""[JSImport("IImported.funSerialized", "space")] [return: JSMarshalAs] internal static partial long JS_Import_Space_IImported_Fun_Serialized (global::System.String str, [JSMarshalAs] long info);"""); + Contains("public static global::Space.Info JS_Import_Space_IImported_Fun (global::System.String str, global::Space.Info info) => Serializer.Deserialize(JS_Import_Space_IImported_Fun_Serialized(str, Serializer.Serialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); } [Fact] @@ -176,9 +176,9 @@ public partial class Class } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static long Bootsharp_Generated_Exports_JSExported_Inv (int _id, int it, [JSMarshalAs] long info) => Serializer.Serialize(Instances.Exported(_id).Inv(Instances.Resolve(it), Serializer.Deserialize(info, SerializerContext.Info)), SerializerContext.Info);"); - Contains("""[JSImport("IImported.funSerialized", "index")] [return: JSMarshalAs] internal static partial long Bootsharp_Generated_Imports_JSImported_Fun_Serialized (int _id, int it, [JSMarshalAs] long info);"""); - Contains("public static global::Info Bootsharp_Generated_Imports_JSImported_Fun (int _id, global::IImported it, global::Info info) => Serializer.Deserialize(Bootsharp_Generated_Imports_JSImported_Fun_Serialized(_id, Instances.Export(it), Serializer.Serialize(info, SerializerContext.Info)), SerializerContext.Info);"); + Contains("[JSExport] [return: JSMarshalAs] internal static long JS_Export_IExported_Inv (int _id, int it, [JSMarshalAs] long info) => Serializer.Serialize(Instances.Exported(_id).Inv(Instances.Resolve(it), Serializer.Deserialize(info, SerializerContext.Info)), SerializerContext.Info);"); + Contains("""[JSImport("IImported.funSerialized", "index")] [return: JSMarshalAs] internal static partial long JS_Import_IImported_Fun_Serialized (int _id, int it, [JSMarshalAs] long info);"""); + Contains("public static global::Info JS_Import_IImported_Fun (int _id, global::IImported it, global::Info info) => Serializer.Deserialize(JS_Import_IImported_Fun_Serialized(_id, Instances.Export(it), Serializer.Serialize(info, SerializerContext.Info)), SerializerContext.Info);"); Contains("[JSExport] internal static async global::System.Threading.Tasks.Task Class_GetExported (int it) => Instances.Export(await global::Class.GetExported(Instances.Resolve(it)));"); Contains("""[JSImport("Class.getImportedSerialized", "index")] internal static partial global::System.Threading.Tasks.Task Class_GetImported_Serialized (int it);"""); } @@ -210,18 +210,18 @@ public interface IImported } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static long Bootsharp_Generated_Exports_Space_JSExported_GetState () => Serializer.Serialize(global::Bootsharp.Generated.Exports.Space.JSExported.GetState(), SerializerContext.Space_Info);"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_SetState ([JSMarshalAs] long value) => global::Bootsharp.Generated.Exports.Space.JSExported.SetState(Serializer.Deserialize(value, SerializerContext.Space_Info));"); + Contains("[JSExport] [return: JSMarshalAs] internal static long JS_Export_Space_IExported_GetState () => Serializer.Serialize(global::Bootsharp.Generated.JS_Export_Space_IExported.GetState(), SerializerContext.Space_Info);"); + Contains("[JSExport] internal static void JS_Export_Space_IExported_SetState ([JSMarshalAs] long value) => global::Bootsharp.Generated.JS_Export_Space_IExported.SetState(Serializer.Deserialize(value, SerializerContext.Space_Info));"); Contains("""[JSImport("IImported.getStateSerialized", "space")] [return: JSMarshalAs] internal static partial long Space_IImported_GetState_Serialized ();"""); - Contains("public static global::Space.Info Bootsharp_Generated_Imports_Space_JSImported_GetState() => Serializer.Deserialize(Space_IImported_GetState_Serialized(), SerializerContext.Space_Info);"); + Contains("public static global::Space.Info JS_Import_Space_IImported_GetState() => Serializer.Deserialize(Space_IImported_GetState_Serialized(), SerializerContext.Space_Info);"); Contains("""[JSImport("IImported.setStateSerialized", "space")] internal static partial void Space_IImported_SetState_Serialized ([JSMarshalAs] long value);"""); - Contains("public static void Bootsharp_Generated_Imports_Space_JSImported_SetState(global::Space.Info value) => Space_IImported_SetState_Serialized(Serializer.Serialize(value, SerializerContext.Space_Info));"); - Contains("[JSExport] internal static global::System.Boolean Bootsharp_Generated_Exports_Space_JSExported_GetActive () => global::Bootsharp.Generated.Exports.Space.JSExported.GetActive();"); + Contains("public static void JS_Import_Space_IImported_SetState(global::Space.Info value) => Space_IImported_SetState_Serialized(Serializer.Serialize(value, SerializerContext.Space_Info));"); + Contains("[JSExport] internal static global::System.Boolean JS_Export_Space_IExported_GetActive () => global::Bootsharp.Generated.JS_Export_Space_IExported.GetActive();"); Contains("""[JSImport("IImported.getActiveSerialized", "space")] internal static partial global::System.Boolean Space_IImported_GetActive_Serialized ();"""); - Contains("public static global::System.Boolean Bootsharp_Generated_Imports_Space_JSImported_GetActive() => Space_IImported_GetActive_Serialized();"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_SetCount (global::System.Int32 value) => global::Bootsharp.Generated.Exports.Space.JSExported.SetCount(value);"); + Contains("public static global::System.Boolean JS_Import_Space_IImported_GetActive() => Space_IImported_GetActive_Serialized();"); + Contains("[JSExport] internal static void JS_Export_Space_IExported_SetCount (global::System.Int32 value) => global::Bootsharp.Generated.JS_Export_Space_IExported.SetCount(value);"); Contains("""[JSImport("IImported.setCountSerialized", "space")] internal static partial void Space_IImported_SetCount_Serialized (global::System.Int32 value);"""); - Contains("public static void Bootsharp_Generated_Imports_Space_JSImported_SetCount(global::System.Int32 value) => Space_IImported_SetCount_Serialized(value);"); + Contains("public static void JS_Import_Space_IImported_SetCount(global::System.Int32 value) => Space_IImported_SetCount_Serialized(value);"); } [Fact] @@ -252,15 +252,15 @@ public class Class } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static long Bootsharp_Generated_Exports_JSExported_GetState (int _id) => Serializer.Serialize(Instances.Exported(_id).State, SerializerContext.Info);"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_JSExported_SetState (int _id, [JSMarshalAs] long value) => Instances.Exported(_id).State = Serializer.Deserialize(value, SerializerContext.Info);"); + Contains("[JSExport] [return: JSMarshalAs] internal static long JS_Export_IExported_GetState (int _id) => Serializer.Serialize(Instances.Exported(_id).State, SerializerContext.Info);"); + Contains("[JSExport] internal static void JS_Export_IExported_SetState (int _id, [JSMarshalAs] long value) => Instances.Exported(_id).State = Serializer.Deserialize(value, SerializerContext.Info);"); Contains("""[JSImport("IImported.getStateSerialized", "index")] [return: JSMarshalAs] internal static partial long IImported_GetState_Serialized (int _id);"""); - Contains("[JSExport] internal static int Bootsharp_Generated_Exports_JSExported_GetExported (int _id) => Instances.Export(Instances.Exported(_id).Exported);"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_JSExported_SetImported (int _id, int value) => Instances.Exported(_id).Imported = Instances.Resolve(value);"); + Contains("[JSExport] internal static int JS_Export_IExported_GetExported (int _id) => Instances.Export(Instances.Exported(_id).Exported);"); + Contains("[JSExport] internal static void JS_Export_IExported_SetImported (int _id, int value) => Instances.Exported(_id).Imported = Instances.Resolve(value);"); Contains("""[JSImport("IImported.getImportedSerialized", "index")] internal static partial int IImported_GetImported_Serialized (int _id);"""); - Contains("public static global::IImported Bootsharp_Generated_Imports_JSImported_GetImported(int _id) => Instances.Resolve(IImported_GetImported_Serialized(_id));"); + Contains("public static global::IImported JS_Import_IImported_GetImported(int _id) => Instances.Resolve(IImported_GetImported_Serialized(_id));"); Contains("""[JSImport("IImported.setExportedSerialized", "index")] internal static partial void IImported_SetExported_Serialized (int _id, int value);"""); - Contains("public static void Bootsharp_Generated_Imports_JSImported_SetExported(int _id, global::IExported value) => IImported_SetExported_Serialized(_id, Instances.Export(value));"); + Contains("public static void JS_Import_IImported_SetExported(int _id, global::IExported value) => IImported_SetExported_Serialized(_id, Instances.Export(value));"); } [Fact] @@ -284,12 +284,12 @@ public interface IImported { event Action Evt; } [ModuleInitializer] internal static unsafe void Initialize () { - global::Bootsharp.Generated.Exports.Space.JSExported.Evt += Handle_Bootsharp_Generated_Exports_Space_JSExported_Evt; + global::Bootsharp.Generated.JS_Export_Space_IExported.Evt += Handle_JS_Export_Space_IExported_Evt; } """); - Contains("void Handle_Bootsharp_Generated_Exports_Space_JSExported_Evt (global::Space.Info obj) => Space_IExported_BroadcastEvt_Serialized(Serializer.Serialize(obj, SerializerContext.Space_Info));"); + Contains("void Handle_JS_Export_Space_IExported_Evt (global::Space.Info obj) => Space_IExported_BroadcastEvt_Serialized(Serializer.Serialize(obj, SerializerContext.Space_Info));"); Contains("""[JSImport("IExported.broadcastEvtSerialized", "space")] internal static partial void Space_IExported_BroadcastEvt_Serialized ([JSMarshalAs] long obj);"""); - Contains("[JSExport] internal static void Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt ([JSMarshalAs] long obj) => ((global::Bootsharp.Generated.Imports.Space.JSImported)Modules.Imports[typeof(global::Space.IImported)].Instance).InvokeEvt(Serializer.Deserialize(obj, SerializerContext.Space_Info));"); + Contains("[JSExport] internal static void JS_Import_Space_IImported_InvokeEvt ([JSMarshalAs] long obj) => ((global::Bootsharp.Generated.JS_Import_Space_IImported)Modules.Imports[typeof(global::Space.IImported)].Instance).InvokeEvt(Serializer.Deserialize(obj, SerializerContext.Space_Info));"); } [Fact] @@ -310,7 +310,7 @@ public partial class Class """)); Execute(); Contains("""[JSImport("IExported.broadcastChangedSerialized", "index")] internal static partial void IExported_BroadcastChanged_Serialized (int _id, [JSMarshalAs] long arg1, int arg2);"""); - Contains("[JSExport] internal static void Bootsharp_Generated_Imports_JSImported_InvokeChanged (int _id, [JSMarshalAs] long arg1, int arg2) => ((global::Bootsharp.Generated.Imports.JSImported)Instances.Resolve(_id)).InvokeChanged(Serializer.Deserialize(arg1, SerializerContext.Record), Instances.Resolve(arg2));"); + Contains("[JSExport] internal static void JS_Import_IImported_InvokeChanged (int _id, [JSMarshalAs] long arg1, int arg2) => ((global::Bootsharp.Generated.JS_Import_IImported)Instances.Resolve(_id)).InvokeChanged(Serializer.Deserialize(arg1, SerializerContext.Record), Instances.Resolve(arg2));"); } [Fact] @@ -473,8 +473,8 @@ [Export] public static void Inv () {} } """)); Execute(); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_Inv () => global::Bootsharp.Generated.Exports.Space.JSExported.Inv();"); - Contains("""[JSImport("IImported.funSerialized", "foo")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized ();"""); + Contains("[JSExport] internal static void JS_Export_Space_IExported_Inv () => global::Bootsharp.Generated.JS_Export_Space_IExported.Inv();"); + Contains("""[JSImport("IImported.funSerialized", "foo")] internal static partial void JS_Import_Space_IImported_Fun_Serialized ();"""); Contains("""[JSImport("Class.broadcastEvtSerialized", "foo")] internal static partial void Space_Class_BroadcastEvt_Serialized ();"""); Contains("void Handle_Space_Class_Evt () => Space_Class_BroadcastEvt_Serialized();"); Contains("[JSExport] internal static void Space_Class_Inv () => global::Space.Class.Inv();"); diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs index e055af97..54bc6507 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs @@ -32,41 +32,37 @@ public interface IExported Execute(); Contains( """ - namespace Bootsharp.Generated + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations { - internal static class ModuleRegistrations + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterModules () - { - Modules.Register(typeof(global::Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::IExported), handler => new global::Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); - } + Modules.Register(typeof(global::Bootsharp.Generated.JS_Export_IExported), new ExportModule(typeof(global::IExported), handler => new global::Bootsharp.Generated.JS_Export_IExported((global::IExported)handler))); } } - namespace Bootsharp.Generated.Exports + public class JS_Export_IExported { - public class JSExported + private static global::IExported handler = null!; + + public JS_Export_IExported (global::IExported handler) { - private static global::IExported handler = null!; - - public JSExported (global::IExported handler) - { - JSExported.handler = handler; - handler.OnRecordChanged += OnRecordChanged.Invoke; - handler.OnSomethingChanged += OnSomethingChanged.Invoke; - } - - [Export] public static event global::System.Action OnRecordChanged; - [Export] public static event global::IExported.SomethingChanged OnSomethingChanged; - [Export] public static global::Record? GetRecord () => handler.Record; - [Export] public static void SetRecord (global::Record? value) => handler.Record = value; - [Export] public static void Inv (global::System.String? a) => handler.Inv(a); - [Export] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); - [Export] public static global::Record? InvRecord () => handler.InvRecord(); - [Export] public static global::System.Threading.Tasks.Task InvAsyncResult () => handler.InvAsyncResult(); - [Export] public static global::System.String[] InvArray (global::System.Int32[] a) => handler.InvArray(a); + JS_Export_IExported.handler = handler; + handler.OnRecordChanged += OnRecordChanged.Invoke; + handler.OnSomethingChanged += OnSomethingChanged.Invoke; } + + [Export] public static event global::System.Action OnRecordChanged; + [Export] public static event global::IExported.SomethingChanged OnSomethingChanged; + [Export] public static global::Record? GetRecord () => handler.Record; + [Export] public static void SetRecord (global::Record? value) => handler.Record = value; + [Export] public static void Inv (global::System.String? a) => handler.Inv(a); + [Export] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); + [Export] public static global::Record? InvRecord () => handler.InvRecord(); + [Export] public static global::System.Threading.Tasks.Task InvAsyncResult () => handler.InvAsyncResult(); + [Export] public static global::System.String[] InvArray (global::System.Int32[] a) => handler.InvArray(a); } """); } @@ -99,41 +95,37 @@ public virtual void Inv (string? a) {} Execute(); Contains( """ - namespace Bootsharp.Generated + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations { - internal static class ModuleRegistrations + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterModules () - { - Modules.Register(typeof(global::Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::Exported), handler => new global::Bootsharp.Generated.Exports.JSExported((global::Exported)handler))); - } + Modules.Register(typeof(global::Bootsharp.Generated.JS_Export_Exported), new ExportModule(typeof(global::Exported), handler => new global::Bootsharp.Generated.JS_Export_Exported((global::Exported)handler))); } } - namespace Bootsharp.Generated.Exports + public class JS_Export_Exported { - public class JSExported + private static global::Exported handler = null!; + + public JS_Export_Exported (global::Exported handler) { - private static global::Exported handler = null!; - - public JSExported (global::Exported handler) - { - JSExported.handler = handler; - handler.OnRecordChanged += OnRecordChanged.Invoke; - handler.OnSomethingChanged += OnSomethingChanged.Invoke; - } - - [Export] public static event global::System.Action OnRecordChanged; - [Export] public static event global::Exported.SomethingChanged OnSomethingChanged; - [Export] public static global::Record? GetRecord () => handler.Record; - [Export] public static void SetRecord (global::Record? value) => handler.Record = value; - [Export] public static void Inv (global::System.String? a) => handler.Inv(a); - [Export] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); - [Export] public static global::Record? InvRecord () => handler.InvRecord(); - [Export] public static global::System.Threading.Tasks.Task InvAsyncResult () => handler.InvAsyncResult(); - [Export] public static global::System.String[] InvArray (global::System.Int32[] a) => handler.InvArray(a); + JS_Export_Exported.handler = handler; + handler.OnRecordChanged += OnRecordChanged.Invoke; + handler.OnSomethingChanged += OnSomethingChanged.Invoke; } + + [Export] public static event global::System.Action OnRecordChanged; + [Export] public static event global::Exported.SomethingChanged OnSomethingChanged; + [Export] public static global::Record? GetRecord () => handler.Record; + [Export] public static void SetRecord (global::Record? value) => handler.Record = value; + [Export] public static void Inv (global::System.String? a) => handler.Inv(a); + [Export] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); + [Export] public static global::Record? InvRecord () => handler.InvRecord(); + [Export] public static global::System.Threading.Tasks.Task InvAsyncResult () => handler.InvAsyncResult(); + [Export] public static global::System.String[] InvArray (global::System.Int32[] a) => handler.InvArray(a); } """); } @@ -182,37 +174,33 @@ public interface IImported Execute(); Contains( """ - namespace Bootsharp.Generated + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations { - internal static class ModuleRegistrations + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterModules () - { - Modules.Register(typeof(global::IImported), new ImportModule(new global::Bootsharp.Generated.Imports.JSImported())); - } + Modules.Register(typeof(global::IImported), new ImportModule(new global::Bootsharp.Generated.JS_Import_IImported())); } } - namespace Bootsharp.Generated.Imports + public class JS_Import_IImported : global::IImported { - public class JSImported : global::IImported + public event global::System.Action OnRecordChanged; + internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); + public event global::IImported.SomethingChanged OnSomethingChanged; + internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); + global::Record? global::IImported.Record { - public event global::System.Action OnRecordChanged; - internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); - public event global::IImported.SomethingChanged OnSomethingChanged; - internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); - global::Record? global::IImported.Record - { - get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetRecord(); - set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetRecord(value); - } - void global::IImported.Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Inv(a); - global::System.Threading.Tasks.Task global::IImported.InvAsync () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvAsync(); - global::Record? global::IImported.InvRecord () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvRecord(); - global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); - global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvArray(a); + get => global::Bootsharp.Generated.Interop.JS_Import_IImported_GetRecord(); + set => global::Bootsharp.Generated.Interop.JS_Import_IImported_SetRecord(value); } + void global::IImported.Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.JS_Import_IImported_Inv(a); + global::System.Threading.Tasks.Task global::IImported.InvAsync () => global::Bootsharp.Generated.Interop.JS_Import_IImported_InvAsync(); + global::Record? global::IImported.InvRecord () => global::Bootsharp.Generated.Interop.JS_Import_IImported_InvRecord(); + global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => global::Bootsharp.Generated.Interop.JS_Import_IImported_InvAsyncResult(); + global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.JS_Import_IImported_InvArray(a); } """); } @@ -251,40 +239,33 @@ public interface IImported { void Fun (Record a); } Execute(); Contains( """ - namespace Bootsharp.Generated + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations { - internal static class ModuleRegistrations + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterModules () - { - Modules.Register(typeof(global::Bootsharp.Generated.Exports.Space.JSExported), new ExportModule(typeof(global::Space.IExported), handler => new global::Bootsharp.Generated.Exports.Space.JSExported((global::Space.IExported)handler))); - Modules.Register(typeof(global::Space.IImported), new ImportModule(new global::Bootsharp.Generated.Imports.Space.JSImported())); - } + Modules.Register(typeof(global::Bootsharp.Generated.JS_Export_Space_IExported), new ExportModule(typeof(global::Space.IExported), handler => new global::Bootsharp.Generated.JS_Export_Space_IExported((global::Space.IExported)handler))); + Modules.Register(typeof(global::Space.IImported), new ImportModule(new global::Bootsharp.Generated.JS_Import_Space_IImported())); } } - namespace Bootsharp.Generated.Exports.Space + public class JS_Export_Space_IExported { - public class JSExported - { - private static global::Space.IExported handler = null!; + private static global::Space.IExported handler = null!; - public JSExported (global::Space.IExported handler) - { - JSExported.handler = handler; - } - - [Export] public static void Inv (global::Space.Record a) => handler.Inv(a); + public JS_Export_Space_IExported (global::Space.IExported handler) + { + JS_Export_Space_IExported.handler = handler; } + + [Export] public static void Inv (global::Space.Record a) => handler.Inv(a); } - namespace Bootsharp.Generated.Imports.Space + public class JS_Import_Space_IImported : global::Space.IImported { - public class JSImported : global::Space.IImported - { - void global::Space.IImported.Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_Space_JSImported_Fun(a); - } + void global::Space.IImported.Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.JS_Import_Space_IImported_Fun(a); } """); } @@ -334,6 +315,6 @@ public interface IShared { void Inv (); } [assembly:Import(typeof(IShared))] """)); Execute(); - Once("class JSShared"); + Once("class JS_Import_IShared"); } } diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs index acdd6e20..0814ec7c 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs @@ -23,7 +23,7 @@ public partial class Class Execute(); Contains( """ - $i.IExported = class JSExported { + $i.IExported = class JS_Export_IExported { constructor(_id) { this._id = _id; } inv(it, info) { return index.IExported.inv(this._id, it, info); } }; @@ -38,10 +38,10 @@ public partial class Class }; export const IImported = { funSerialized: (_id, it, info) => serialize($i.imported(_id).fun($i.resolve(it, $i.IImported), deserialize(info, $s.Info)), $s.Info), - fun: (_id, it, info) => deserialize(exports.Bootsharp_Generated_Exports_JSImported_Fun(_id, $i.import(it), serialize(info, $s.Info)), $s.Info) + fun: (_id, it, info) => deserialize(exports.JS_Export_IImported_Fun(_id, $i.import(it), serialize(info, $s.Info)), $s.Info) }; export const IExported = { - inv: (_id, it, info) => deserialize(exports.Bootsharp_Generated_Exports_JSExported_Inv(_id, $i.import(it), serialize(info, $s.Info)), $s.Info), + inv: (_id, it, info) => deserialize(exports.JS_Export_IExported_Inv(_id, $i.import(it), serialize(info, $s.Info)), $s.Info), invSerialized: (_id, it, info) => serialize($i.imported(_id).inv($i.resolve(it, $i.IExported), deserialize(info, $s.Info)), $s.Info) }; """); @@ -77,7 +77,7 @@ public partial class Class Execute(); Contains( """ - $i.IExported = class JSExported { + $i.IExported = class JS_Export_IExported { constructor(_id) { this._id = _id; } get state() { return index.IExported.getState(this._id); } set state(value) { index.IExported.setState(this._id, value); } @@ -100,10 +100,10 @@ public partial class Class setExportedSerialized(_id, value) { $i.imported(_id).exported = $i.resolve(value, $i.IExported); } }; export const IExported = { - getState(_id) { return deserialize(exports.Bootsharp_Generated_Exports_JSExported_GetState(_id), $s.Info) ?? undefined; }, - setState(_id, value) { exports.Bootsharp_Generated_Exports_JSExported_SetState(_id, serialize(value, $s.Info)); }, - getExported(_id) { return $i.resolve(exports.Bootsharp_Generated_Exports_JSExported_GetExported(_id), $i.IExported); }, - setImported(_id, value) { exports.Bootsharp_Generated_Exports_JSExported_SetImported(_id, $i.import(value)); } + getState(_id) { return deserialize(exports.JS_Export_IExported_GetState(_id), $s.Info) ?? undefined; }, + setState(_id, value) { exports.JS_Export_IExported_SetState(_id, serialize(value, $s.Info)); }, + getExported(_id) { return $i.resolve(exports.JS_Export_IExported_GetExported(_id), $i.IExported); }, + setImported(_id, value) { exports.JS_Export_IExported_SetImported(_id, $i.import(value)); } }; """); } @@ -127,7 +127,7 @@ public partial class Class Execute(); Contains( """ - $i.IExported = class JSExported { + $i.IExported = class JS_Export_IExported { constructor(_id) { this._id = _id; } changed = new Event(); broadcastChanged(arg1, arg2) { this.changed.broadcast(arg1, arg2); } @@ -142,7 +142,7 @@ public partial class Class it.changed.unsubscribe(handleChanged); }; - function handleChanged(arg1, arg2) { exports.Bootsharp_Generated_Imports_JSImported_InvokeChanged(_id, $i.import_IImported(arg1), serialize(arg2, $s.Info)); } + function handleChanged(arg1, arg2) { exports.JS_Import_IImported_InvokeChanged(_id, $i.import_IImported(arg1), serialize(arg2, $s.Info)); } }); }; """); @@ -220,7 +220,7 @@ public class Class } """)); Execute(); - Contains("$i.IExportedInstanced = class JSExportedInstanced"); + Contains("$i.IExportedInstanced = class JS_Export_IExportedInstanced"); Contains("$i.import_IImportedInstanced = function"); DoesNotContain("index.g.mjs", "$i.IExportedInstanced = class"); DoesNotContain("index.g.mjs", "$i.import_IImportedInstanced = function"); @@ -249,7 +249,7 @@ public partial class Class """ import * as foo_bar from "./modules/foo/bar.g.mjs"; - $i.Foo_Bar_IExported = class Foo_Bar_JSExported { + $i.Foo_Bar_IExported = class JS_Export_Foo_Bar_IExported { constructor(_id) { this._id = _id; } get state() { return foo_bar.IExported.getState(this._id); } method() { foo_bar.IExported.method(this._id); } diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index d655ed78..a313d5be 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -26,7 +26,7 @@ [Import] public static void Fun () {} Execute(); Contains("""getExport("Class_InvokeEvt")"""); Contains("""getExport("Class_InvAsync")"""); - Contains("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetState")"""); + Contains("""getExport("JS_Export_IExportedStatic_GetState")"""); Contains("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); } @@ -431,7 +431,7 @@ public interface IImported { Info Fun (string str, Info info); } Contains("space.g.mjs", """ export const IExported = { - inv: (str, info) => deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(str, serialize(info, $s.Space_Info)), $s.Space_Info) + inv: (str, info) => deserialize(exports.JS_Export_Space_IExported_Inv(str, serialize(info, $s.Space_Info)), $s.Space_Info) }; export const IImported = { get fun() { return this.funHandler; }, @@ -469,9 +469,9 @@ public interface IImported Contains("space.g.mjs", """ export const IExported = { - get state() { return deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_GetState(), $s.Space_Info) ?? undefined; }, - set state(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetState(serialize(value, $s.Space_Info)); }, - set count(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetCount(value); } + get state() { return deserialize(exports.JS_Export_Space_IExported_GetState(), $s.Space_Info) ?? undefined; }, + set state(value) { exports.JS_Export_Space_IExported_SetState(serialize(value, $s.Space_Info)); }, + set count(value) { exports.JS_Export_Space_IExported_SetCount(value); } }; export const IImported = { getStateSerialized() { return serialize(this.state.get(), $s.Space_Info); }, @@ -504,7 +504,7 @@ public interface IImported { event Action Evt; } broadcastEvtSerialized: (obj) => IExported.evt.broadcast(deserialize(obj, $s.Space_Info)) }; export const IImported = { - evt: importEvent((obj) => exports.Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt(serialize(obj, $s.Space_Info))) + evt: importEvent((obj) => exports.JS_Import_Space_IImported_InvokeEvt(serialize(obj, $s.Space_Info))) }; """); } @@ -809,9 +809,9 @@ public interface IImported export const Foo = { quz: new Event(), broadcastChangedSerialized: () => Foo.quz.broadcast(), - get qux() { return deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_GetState(), $s.Space_Enum); }, - set qux(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetState(serialize(value, $s.Space_Enum)); }, - bar: (e) => exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(serialize(e, $s.Space_Enum)), + get qux() { return deserialize(exports.JS_Export_Space_IExported_GetState(), $s.Space_Enum); }, + set qux(value) { exports.JS_Export_Space_IExported_SetState(serialize(value, $s.Space_Enum)); }, + bar: (e) => exports.JS_Export_Space_IExported_Inv(serialize(e, $s.Space_Enum)), get baz() { return this.bazHandler; }, set baz(handler) { this.bazHandler = handler; this.bazSerializedHandler = (e) => this.bazHandler(deserialize(e, $s.Space_Enum)); }, get bazSerialized() { return this.bazSerializedHandler; } @@ -862,9 +862,9 @@ public class Class }; export const Foo = { broadcastEventSerialized: (_id) => $i.resolve(_id, $i.Space_IInst).broadcastEvent(), - getProperty(_id) { return deserialize(exports.Bootsharp_Generated_Exports_Space_JSInst_GetProperty(_id), $s.Space_Enum); }, - setProperty(_id, value) { exports.Bootsharp_Generated_Exports_Space_JSInst_SetProperty(_id, serialize(value, $s.Space_Enum)); }, - bar: (_id, e) => exports.Bootsharp_Generated_Exports_Space_JSInst_Method(_id, serialize(e, $s.Space_Enum)) + getProperty(_id) { return deserialize(exports.JS_Export_Space_IInst_GetProperty(_id), $s.Space_Enum); }, + setProperty(_id, value) { exports.JS_Export_Space_IInst_SetProperty(_id, serialize(value, $s.Space_Enum)); }, + bar: (_id, e) => exports.JS_Export_Space_IInst_Method(_id, serialize(e, $s.Space_Enum)) }; export const Enum = { "0": "A", diff --git a/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs index aa17d30f..ac1f7d32 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs @@ -15,31 +15,30 @@ public string Generate (IReadOnlyCollection its) => using System.Runtime.CompilerServices; using System.Runtime.InteropServices.JavaScript; - namespace Bootsharp.Generated + namespace Bootsharp.Generated; + + public static partial class Instances { - public static partial class Instances + internal static int Export (T it, Bootsharp.Instances.ExportCallback? cb = null) where T : class => Bootsharp.Instances.Export(it, cb); + internal static T Exported (int id) where T : class => Bootsharp.Instances.Exported(id); + internal static T Resolve (int id) where T : class => Bootsharp.Instances.Resolve(id); + + internal static void DisposeImported (int id) + { + NotifyImportedDisposed(id); + Bootsharp.Instances.DisposeImported(id); + } + + [ModuleInitializer] + internal static void RegisterImports () { - internal static int Export (T it, Bootsharp.Instances.ExportCallback? cb = null) where T : class => Bootsharp.Instances.Export(it, cb); - internal static T Exported (int id) where T : class => Bootsharp.Instances.Exported(id); - internal static T Resolve (int id) where T : class => Bootsharp.Instances.Resolve(id); - - internal static void DisposeImported (int id) - { - NotifyImportedDisposed(id); - Bootsharp.Instances.DisposeImported(id); - } - - [ModuleInitializer] - internal static void RegisterImports () - { - {{Fmt(its.Where(i => i.IK == InteropKind.Import).Select(EmitImporter), 3)}} - } - - {{Fmt(its.Where(i => i.Exporter != null).Select(EmitExporter), 2, "\n\n")}} - - [JSExport] private static void DisposeExported (int id) => Bootsharp.Instances.DisposeExported(id); - [JSImport("instances.disposeImported", "Bootsharp")] private static partial void NotifyImportedDisposed (int id); + {{Fmt(its.Where(i => i.IK == InteropKind.Import).Select(EmitImporter), 2)}} } + + {{Fmt(its.Where(i => i.Exporter != null).Select(EmitExporter), 1, "\n\n")}} + + [JSExport] private static void DisposeExported (int id) => Bootsharp.Instances.DisposeExported(id); + [JSImport("instances.disposeImported", "Bootsharp")] private static partial void NotifyImportedDisposed (int id); } {{Fmt(its.Where(i => i.IK == InteropKind.Import).Select(EmitProxy), 0, "\n\n")}} @@ -74,14 +73,11 @@ private static string EmitExporter (InstanceMeta it) private string EmitProxy (InstanceMeta it) => $$""" - namespace {{(this.it = it).Proxy.Space}} + public class {{(this.it = it).Proxy.Id}} (int id) : global::Bootsharp.JSProxy(id), {{it.Syntax}} { - public class {{it.Proxy.Name}} (int id) : global::Bootsharp.JSProxy(id), {{it.Syntax}} - { - ~{{it.Proxy.Name}}() => Instances.DisposeImported(_id); + ~{{it.Proxy.Id}}() => Instances.DisposeImported(_id); - {{Fmt(it.Members.Select(EmitMemberImport), 2)}} - } + {{Fmt(it.Members.Select(EmitMemberImport))}} } """; diff --git a/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs index 3f3836e2..d9c81e9c 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs @@ -12,15 +12,14 @@ public string Generate (IReadOnlyCollection mds) => #nullable enable #pragma warning disable - namespace Bootsharp.Generated + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations { - internal static class ModuleRegistrations + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterModules () - { - {{Fmt(mds.Select(EmitRegistration), 3)}} - } + {{Fmt(mds.Select(EmitRegistration), 2)}} } } @@ -47,33 +46,27 @@ private string EmitModule (ModuleMeta md) private string EmitModuleExport () => $$""" - namespace {{md.Proxy.Space}} + public class {{md.Proxy.Id}} { - public class {{md.Proxy.Name}} - { - private static {{md.Syntax}} handler = null!; + private static {{md.Syntax}} handler = null!; - public {{md.Proxy.Name}} ({{md.Syntax}} handler) - { - {{Fmt([ - $"{md.Proxy.Name}.handler = handler;", - ..md.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") - ], 3)}} - } - - {{Fmt(md.Members.Select(EmitMemberExport), 2)}} + public {{md.Proxy.Id}} ({{md.Syntax}} handler) + { + {{Fmt([ + $"{md.Proxy.Id}.handler = handler;", + ..md.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") + ], 2)}} } + + {{Fmt(md.Members.Select(EmitMemberExport))}} } """; private string EmitModuleImport () => $$""" - namespace {{md.Proxy.Space}} + public class {{md.Proxy.Id}} : {{md.Syntax}} { - public class {{md.Proxy.Name}} : {{md.Syntax}} - { - {{Fmt(md.Members.Select(EmitMemberImport), 2)}} - } + {{Fmt(md.Members.Select(EmitMemberImport))}} } """; diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs index 190f0fe2..9b08a7c4 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs @@ -51,7 +51,7 @@ string EmitHandler (EventMeta e) private string EmitProxy (InstanceMeta it) => $$""" - $i.{{it.Id}} = class {{it.Proxy.JS}} { + $i.{{it.Id}} = class {{it.Proxy.Id}} { {{Fmt([ "constructor(_id) { this._id = _id; }", ..it.Members.Select(EmitMember) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 64bba907..47028c88 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.329 + 0.8.0-alpha.330 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test.Library/Registries/Registries.cs b/src/js/test/cs/Test.Library/Registries/Registries.cs index 818fd1a8..fa5e0c2e 100644 --- a/src/js/test/cs/Test.Library/Registries/Registries.cs +++ b/src/js/test/cs/Test.Library/Registries/Registries.cs @@ -8,7 +8,7 @@ namespace Test.Library; public partial class Registries { - [Export] public static Func GetFunc () => () => "XXX"; + // [Export] public static Func GetFunc () => () => "XXX"; [Export] public static event Action? OnVehicleBroadcast; From 12af78dc75f2ba209eb6c43ad3c253fb9574d8c0 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 17:07:49 +0300 Subject: [PATCH 5/9] fix generic declarations --- .../Attributes/PreferencesAttribute.cs | 2 +- .../GenerateJS/DeclarationTest.cs | 54 ++++++++++++------- .../Common/Global/GlobalType.cs | 36 +++++++------ .../Common/Inspection/Meta/TypeMeta.cs | 10 ++-- .../Declarations/DeclarationGenerator.cs | 17 +++--- .../Declarations/TypeSyntaxBuilder.cs | 3 +- src/cs/Directory.Build.props | 2 +- .../cs/Test.Library/Registries/Registries.cs | 2 - 8 files changed, 72 insertions(+), 54 deletions(-) diff --git a/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs b/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs index 9a4160e3..a422874d 100644 --- a/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs @@ -30,7 +30,7 @@ public sealed class PreferencesAttribute : Attribute /// Customize how C# type names transform into JavaScript object names. /// /// - /// The patterns are matched against the C# type names, with generic identity removed. + /// The patterns are matched against the C# type names. /// public string[] Name { get; init; } = []; /// diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index 8b82c698..b90dab78 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -72,7 +72,7 @@ export namespace Foo { } [Fact] - public void CrawledTypeDoesNotOverrideSpecializedDeclaration () + public void CrawledTypeDoesNotOverrideSpecialized () { AddAssembly(With( """ @@ -115,7 +115,7 @@ public class Class } [Fact] - public void FunctionDeclarationIsExportedForInvokableMethod () + public void FunctionIsExportedForInvokableMethod () { AddAssembly(WithClass("Foo", "[Export] public static void Foo () { }")); Execute(); @@ -354,7 +354,7 @@ public void OtherTypesAreTranslatedToAny () } [Fact] - public void DefinitionIsGeneratedForObjectType () + public void GeneratedForObjectType () { AddAssembly( With("n", "public class Foo { public string S { get; set; } public int I { get; set; } }"), @@ -373,7 +373,7 @@ export interface Foo { } [Fact] - public void DefinitionIsGeneratedForInterfaceAndImplementation () + public void GeneratedForInterfaceAndImplementation () { AddAssembly( With("n", "public interface Interface { Interface Foo { get; } void Bar (Interface b); }"), @@ -400,7 +400,7 @@ export interface Interface { } [Fact] - public void DefinitionIsGeneratedForTypeWithListProperty () + public void GeneratedForTypeWithListProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -421,7 +421,7 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithJaggedArrayProperty () + public void GeneratedForTypeWithJaggedArrayProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -442,7 +442,7 @@ export interface Item { } [Fact] - public void DefinitionIsGeneratedForTypeWithReadOnlyListProperty () + public void GeneratedForTypeWithReadOnlyListProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -463,7 +463,7 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithDictionaryProperty () + public void GeneratedForTypeWithDictionaryProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -484,7 +484,7 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithReadOnlyDictionaryProperty () + public void GeneratedForTypeWithReadOnlyDictionaryProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -505,7 +505,7 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithCollectionProperty () + public void GeneratedForTypeWithCollectionProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -526,7 +526,7 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithReadOnlyCollectionProperty () + public void GeneratedForTypeWithReadOnlyCollectionProperty () { AddAssembly( With("n", "public interface Item { }"), @@ -547,7 +547,7 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForGenericClass () + public void GeneratedForGenericClass () { AddAssembly( With("n", "public class Generic where T: notnull { public required T Value { get; set; } }"), @@ -570,7 +570,7 @@ export interface GenericNull { } [Fact] - public void DefinitionIsGeneratedForGenericRecord () + public void GeneratedForGenericRecord () { AddAssembly( With("n", "public record Generic where T: notnull { public T Value { get; set; } }"), @@ -592,7 +592,7 @@ export namespace Class { } [Fact] - public void DefinitionIsGeneratedForGenericInterface () + public void GeneratedForGenericInterface () { AddAssembly( With("n", "public interface IGenericInterface { public T Value { get; set; } }"), @@ -610,7 +610,7 @@ export interface IGenericInterface { } [Fact] - public void DefinitionIsGeneratedForNestedGenericTypes () + public void GeneratedForNestedGenericTypes () { AddAssembly( With("Foo", "public class GenericClass { public T Value { get; set; } }"), @@ -626,7 +626,7 @@ export namespace Class { } [Fact] - public void DefinitionIsGeneratedForGenericClassWithMultipleTypeArguments () + public void GeneratedForGenericClassWithMultipleTypeArguments () { AddAssembly( With("n", "public class GenericClass { public T1 Key { get; set; } public T2 Value { get; set; } }"), @@ -644,6 +644,22 @@ export interface GenericClass { """); } + [Fact] + public void DoesNotDuplicateGenerics () + { + AddAssembly( + With("public interface IGeneric { public T Value { get; set; } }"), + With("public record GenericRecord (T Value);"), + WithClass( + """ + [Export] public static void Foo (IGeneric a, IGeneric b) { } + [Export] public static void Bar (GenericRecord a, GenericRecord b) { } + """)); + Execute(); + Once("export interface IGeneric"); + Once("export type GenericRecord"); + } + [Fact] public void CanCrawlCustomTypes () { @@ -1231,7 +1247,7 @@ public class Class } [Fact] - public void DeclarationsCrossNamespaceImportsEmitted () + public void CrossNamespaceImportsEmitted () { AddAssembly(With( """ @@ -1248,7 +1264,7 @@ public class Class { } [Fact] - public void DeclarationFileImportsRootNamespaceTypeFromPackageRoot () + public void GlobalNamespaceImportsFromIndex () { AddAssembly(With( """ @@ -1264,7 +1280,7 @@ public class Class { [Export] public static RootRecord Get () => default!; } } [Fact] - public void TypeDeclarationGroupsMultipleNestedTypes () + public void GroupsMultipleNestedTypes () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 9bd49c5e..3be5a835 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -108,39 +108,41 @@ public static string PrependIdArg (string args) return $"_id, {args}"; } - public static string BuildId (Type type) + public static string BuildId (Type type, bool full = true, char separator = '_') { - var builder = new StringBuilder(); - foreach (var c in BuildSyntax(type).Replace("global::", "")) - if (char.IsLetterOrDigit(c) || c == '_') builder.Append(c); - else if (c == '.') builder.Append('_'); - else if (c == '?') builder.Append("OrNull"); - else if (c == '[') builder.Append("Array"); - else if (c == '<') builder.Append("_Of_"); - else if (c == ',') builder.Append("_And_"); - return builder.ToString(); + var sb = new StringBuilder(); + foreach (var c in BuildSyntax(type, full: full).Replace("global::", "")) + if (char.IsLetterOrDigit(c) || c == separator) sb.Append(c); + else if (c == '.') sb.Append(separator); + else if (c == '?') sb.Append("OrNull"); + else if (c == '[') sb.Append("Array"); + else if (c == '<') sb.Append("_Of_"); + else if (c == ',') sb.Append("_And_"); + return sb.ToString(); } - public static string BuildSyntax (Type type, NullabilityInfo? nul = null, bool forceNil = false) + public static string BuildSyntax (Type type, NullabilityInfo? nul = null, bool forceNil = false, bool full = true) { var nil = (forceNil || nul?.ReadState == NullabilityState.Nullable) ? "?" : ""; + var global = full ? "global::" : ""; if (IsVoid(type)) return "void"; if (type.IsArray) return $"{BuildSyntax(type.GetElementType()!, nul?.ElementType)}[]{nil}"; if (type.IsGenericType) return BuildGeneric(type, type.GenericTypeArguments); - return $"global::{ResolveTypeName(type)}{nil}"; + return $"{global}{ResolveTypeName(type)}{nil}"; string BuildGeneric (Type type, Type[] args) { - if (IsNullable(type, out var value)) return BuildSyntax(value, nul, true); + if (IsNullable(type, out var value)) return BuildSyntax(value, nul, true, full); var name = TrimGeneric(ResolveTypeName(type)); - var typeArgs = string.Join(", ", args.Select((a, i) => BuildSyntax(a, nul?.GenericTypeArguments[i]))); - return $"global::{name}<{typeArgs}>"; + var typeArgs = string.Join(", ", args.Select((a, i) => + BuildSyntax(a, nul?.GenericTypeArguments[i], forceNil, full))); + return $"{global}{name}<{typeArgs}>"; } - static string ResolveTypeName (Type type) + string ResolveTypeName (Type type) { if (type.IsNested) return $"{ResolveTypeName(type.DeclaringType!)}.{type.Name}"; - if (type.Namespace is null) return type.Name; + if (!full || type.Namespace is null) return type.Name; return $"{type.Namespace}.{type.Name}"; } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/TypeMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/TypeMeta.cs index 034a2e3e..9047856d 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/TypeMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/TypeMeta.cs @@ -27,16 +27,14 @@ internal record TypeMeta (Type Clr) /// public string JSNode { get; } = BuildNode(Clr); - private static string BuildModule (Type Clr) + private static string BuildModule (Type clr) { - var slug = Slugify(WithPref(Pref.Space, Clr.Namespace ?? "")); + var slug = Slugify(WithPref(Pref.Space, clr.Namespace ?? "")); return string.IsNullOrWhiteSpace(slug) ? "index" : slug; } - private static string BuildNode (Type Clr) + private static string BuildNode (Type clr) { - var full = TrimGeneric(Clr.FullName!); - var name = full[(full.LastIndexOf('.') + 1)..]; - return WithPref(Pref.Name, name).Replace('+', '.'); + return WithPref(Pref.Name, clr.Name, BuildId(clr, false, '.')); } } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs index 9dc28130..9f6d7fd0 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -4,6 +4,7 @@ namespace Bootsharp.Publish; internal sealed class DeclarationGenerator { + private readonly HashSet declared = []; private readonly CodeBuilder bld = new(); private readonly TypeSyntaxBuilder ts; private readonly DocumentationBuilder doc; @@ -21,6 +22,7 @@ public DeclarationGenerator (SolutionInspection spec, JSModules mds) public string Generate (JSModule module) { bld.Clear(); + declared.Clear(); ts.EnterModule(module); foreach (var node in module.Nodes) DeclareNode(node); @@ -42,13 +44,14 @@ private void DeclareNode (JSNode node) if (surf != null) doc.Type(surf); bld.Enter($"export namespace {node.Name} {{"); } - // Distinct by CLR to discard the other side of a bidirectional (export+import) - // instance surface, because both produce identical declarations. - foreach (var type in node.Types.DistinctBy(t => t.Clr)) - if (type is SerializedEnumMeta enu) DeclareEnum(enu); - else if (type is SerializedObjectMeta o) DeclareSerialized(o); - else if (type is InstanceMeta it) DeclareInstance(it); - else if (type is SurfaceMeta srf) DeclareSurface(srf); + foreach (var type in node.Types) + // Dedup by CLR to discard the other side of a bidirectional (export+import) + // instance surface and closed generic variants (all produce same open type). + if (declared.Add(type.Clr.IsGenericType ? type.Clr.GetGenericTypeDefinition() : type.Clr)) + if (type is SerializedEnumMeta enu) DeclareEnum(enu); + else if (type is SerializedObjectMeta o) DeclareSerialized(o); + else if (type is InstanceMeta it) DeclareInstance(it); + else if (type is SurfaceMeta srf) DeclareSurface(srf); foreach (var child in node.Children) DeclareNode(child); if (wrap) bld.Exit("}"); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs index 8520476d..520ac1ee 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs @@ -128,8 +128,9 @@ private string BuildUser (Type type) var @ref = mds.Ref(type, module); if (!type.IsGenericType) return @ref; EnterNullity(); + var name = @ref[..@ref.IndexOf("_Of_", StringComparison.Ordinal)]; var args = string.Join(", ", type.GetGenericArguments().Select(Build)); - return $"{@ref}<{args}>"; + return $"{name}<{args}>"; } private string BuildPrimitive (Type type) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 47028c88..8536be22 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.330 + 0.8.0-alpha.331 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test.Library/Registries/Registries.cs b/src/js/test/cs/Test.Library/Registries/Registries.cs index fa5e0c2e..ecd09614 100644 --- a/src/js/test/cs/Test.Library/Registries/Registries.cs +++ b/src/js/test/cs/Test.Library/Registries/Registries.cs @@ -8,8 +8,6 @@ namespace Test.Library; public partial class Registries { - // [Export] public static Func GetFunc () => () => "XXX"; - [Export] public static event Action? OnVehicleBroadcast; public static IRegistryProvider Provider { get; set; } = null!; From d2100c587e92936f4c73deb39089d3010c2948f9 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 19:18:30 +0300 Subject: [PATCH 6/9] implement delegates support --- docs/guide/declarations.md | 216 +++++++++++------- src/cs/Bootsharp.Common.Test/InstancesTest.cs | 23 ++ src/cs/Bootsharp.Common/Interop/Instances.cs | 9 +- .../{InstancesTest.cs => CSInstanceTest.cs} | 81 ++++++- .../{InteropTest.cs => CSInteropTest.cs} | 2 +- .../{ModulesTest.cs => CSModuleTest.cs} | 2 +- ...{SerializerTest.cs => CSSerializerTest.cs} | 2 +- .../GenerateJS/DeclarationTest.cs | 183 +++++++++------ .../GenerateJS/JSInstanceTest.cs | 55 +++++ .../Common/Global/GlobalType.cs | 8 +- .../Common/Inspection/Meta/SurfaceMeta.cs | 14 ++ .../Common/Inspection/TypeInspector.cs | 15 +- ...nceGenerator.cs => CSInstanceGenerator.cs} | 37 ++- ...eropGenerator.cs => CSInteropGenerator.cs} | 8 +- ...oduleGenerator.cs => CSModuleGenerator.cs} | 4 +- ...rGenerator.cs => CSSerializerGenerator.cs} | 6 +- .../GenerateCS/GenerateCS.cs | 8 +- .../Declarations/DeclarationGenerator.cs | 20 +- .../Declarations/DocumentationBuilder.cs | 3 + .../Declarations/TypeSyntaxBuilder.cs | 87 ++++--- .../GenerateJS/DotNetPatcher.cs | 3 + .../GenerateJS/JSImportsGenerator.cs | 3 + .../GenerateJS/JSInstanceGenerator.cs | 33 ++- .../GenerateJS/JSModuleGenerator.cs | 11 +- .../GenerateJS/JSModules/JSModules.cs | 2 +- .../GenerateJS/JSSerializerGenerator.cs | 4 + .../GenerateJS/ResourceGenerator.cs | 3 + src/cs/Directory.Build.props | 2 +- src/js/src/instances.mts | 10 +- src/js/test/cs/Test.Library/Assertions.cs | 5 +- .../cs/Test.Library/Modules/Bidirectional.cs | 6 +- .../cs/Test.Library/Modules/ExportedModule.cs | 5 +- .../cs/Test.Library/Modules/IBidirectional.cs | 6 +- .../Test.Library/Modules/IExportedModule.cs | 3 +- .../Test.Library/Modules/IImportedModule.cs | 3 +- .../test/cs/Test.Library/Modules/Modules.cs | 7 +- src/js/test/cs/Test/Serialization.cs | 16 +- src/js/test/spec/interop.spec.ts | 16 +- src/js/test/spec/serialization.spec.ts | 16 +- 39 files changed, 678 insertions(+), 259 deletions(-) rename src/cs/Bootsharp.Publish.Test/GenerateCS/{InstancesTest.cs => CSInstanceTest.cs} (58%) rename src/cs/Bootsharp.Publish.Test/GenerateCS/{InteropTest.cs => CSInteropTest.cs} (99%) rename src/cs/Bootsharp.Publish.Test/GenerateCS/{ModulesTest.cs => CSModuleTest.cs} (99%) rename src/cs/Bootsharp.Publish.Test/GenerateCS/{SerializerTest.cs => CSSerializerTest.cs} (99%) rename src/cs/Bootsharp.Publish/GenerateCS/{InstanceGenerator.cs => CSInstanceGenerator.cs} (74%) rename src/cs/Bootsharp.Publish/GenerateCS/{InteropGenerator.cs => CSInteropGenerator.cs} (97%) rename src/cs/Bootsharp.Publish/GenerateCS/{ModuleGenerator.cs => CSModuleGenerator.cs} (97%) rename src/cs/Bootsharp.Publish/GenerateCS/{SerializerGenerator.cs => CSSerializerGenerator.cs} (95%) diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index 6ca7372f..e0ddaa5a 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -1,25 +1,23 @@ # Type Declarations -Bootsharp will automatically generate [type declarations](https://www.typescriptlang.org/docs/handbook/2/type-declarations) for interop APIs when building the solution. One `.g.d.mts` file is emitted per C# namespace, colocated with the matching `.g.mjs` binding under the `generated/` directory of the compiled module package. +Bootsharp will automatically generate [type declarations](https://www.typescriptlang.org/docs/handbook/2/type-declarations) for interop APIs when building the solution. One `.g.d.mts` file is emitted per C# namespace, colocated with the matching `.g.mjs` binding under the `generated/modules` directory of the compiled module package. ## Function Declarations For interop methods, function declarations are emitted under the class's TS namespace wrapper inside the C# namespace's module: ```csharp -namespace Foo; - -public class Bar +public class Class { [Export] public static void Baz() { } } ``` -— will make the following emitted in `generated/foo.g.d.mts`: +— will make the following emitted in `generated/modules/index.g.d.mts`: ```ts -export namespace Bar { +export namespace Class { export function baz(): void; } ``` @@ -27,35 +25,33 @@ export namespace Bar { — which allows consuming the API in JavaScript as: ```ts -import { Bar } from "bootsharp/foo"; +import { Class } from "bootsharp"; -Bar.baz(); +Class.baz(); ``` Imported methods will be emitted as properties, which have to be assigned before booting the runtime: ::: code-group -```csharp [Bar.cs] -namespace Foo; - -public partial class Bar +```csharp [Class.cs] +public partial class Class { [Import] public static partial void Baz(); } ``` -```ts [foo.g.d.mts] -export namespace Bar { +```ts [index.g.d.mts] +export namespace Class { export let baz: () => void; } ``` ```ts [main.ts] -import { Bar } from "bootsharp/foo"; +import { Class } from "bootsharp"; -Bar.baz = () => {}; +Class.baz = () => {}; ``` ::: @@ -66,10 +62,8 @@ JavaScript does not have function overloads, so Bootsharp automatically disambig ::: code-group -```csharp [Bar.cs] -namespace Foo; - -public class Bar +```csharp [Class.cs] +public class Class { [Export] public static void Start (string title) {} [Export] public static void Start (string title, string info) {} @@ -78,8 +72,8 @@ public class Bar } ``` -```ts [foo.g.d.mts] -export namespace Bar { +```ts [index.g.d.mts] +export namespace Class { export function start(title: string): void; export function startWithInfo(title: string, info: string): void; export function startWithProgress(title: string, progress: number): void; @@ -89,32 +83,59 @@ export namespace Bar { ::: +## Default Arguments + +C# method parameters with default values are emitted as optional TypeScript parameters using the `?:` syntax, letting callers omit them at the call site: + +::: code-group + +```csharp [Class.cs] +public class Class +{ + [Export] + public static void Greet (string name, string greeting = "Hello") {} +} +``` + +```ts [index.g.d.mts] +export namespace Class { + export function greet(name: string, greeting?: string): void; +} +``` + +```ts [main.ts] +import { Class } from "bootsharp"; + +Class.greet("World"); +Class.greet("World", "Hi"); +``` + +::: + ## Property Declarations Exported properties are emitted as variables under the declaring class's TS namespace: ::: code-group -```csharp [Bar.cs] -namespace Foo; - -public class Bar +```csharp [Class.cs] +public class Class { [Export] public static string Baz { get; set; } = ""; } ``` -```ts [foo.g.d.mts] -export namespace Bar { +```ts [index.g.d.mts] +export namespace Class { export let baz: string; } ``` ```ts [main.ts] -import { Bar } from "bootsharp/foo"; +import { Class } from "bootsharp"; -Bar.baz = "updated"; +Class.baz = "updated"; ``` ::: @@ -123,27 +144,25 @@ Imported properties are emitted as accessor pairs, which have to be assigned bef ::: code-group -```csharp [Bar.cs] -namespace Foo; - -public static partial class Bar +```csharp [Class.cs] +public static partial class Class { [Import] public static partial string Baz { get; set; } } ``` -```ts [foo.g.d.mts] -export namespace Bar { +```ts [index.g.d.mts] +export namespace Class { export let baz: { get: () => string; set: (value: string) => void }; } ``` ```ts [main.ts] -import { Bar } from "bootsharp/foo"; +import { Class } from "bootsharp"; let baz = ""; -Bar.baz = { get: () => baz, set: value => baz = value }; +Class.baz = { get: () => baz, set: value => baz = value }; ``` ::: @@ -154,26 +173,24 @@ Exported events are emitted as `EventSubscriber` objects: ::: code-group -```csharp [Bar.cs] -namespace Foo; - -public class Bar +```csharp [Class.cs] +public class Class { [Export] public static event Action? OnBaz; } ``` -```ts [foo.g.d.mts] -export namespace Bar { +```ts [index.g.d.mts] +export namespace Class { export const onBaz: EventSubscriber<[payload: string]>; } ``` ```ts [main.ts] -import { Bar } from "bootsharp/foo"; +import { Class } from "bootsharp"; -Bar.onBaz.subscribe(payload => {}); +Class.onBaz.subscribe(payload => {}); ``` ::: @@ -182,26 +199,83 @@ Imported events are emitted as `EventBroadcaster` objects: ::: code-group -```csharp [Bar.cs] -namespace Foo; - -public static partial class Bar +```csharp [Class.cs] +public static partial class Class { [Import] public static event Action? OnBaz; } ``` -```ts [foo.g.d.mts] -export namespace Bar { +```ts [index.g.d.mts] +export namespace Class { export const onBaz: EventBroadcaster<[payload: string]>; } ``` ```ts [main.ts] -import { Bar } from "bootsharp/foo"; +import { Class } from "bootsharp"; -Bar.onBaz.broadcast("updated"); +Class.onBaz.broadcast("updated"); +``` + +::: + +## Delegate Declarations + +Custom delegates are emitted as TypeScript function-type aliases: + +::: code-group + +```csharp [Class.cs] +public delegate void Notify (string msg); + +public class Class +{ + [Export] + public static Notify GetNotify () => msg => Console.WriteLine(msg); +} +``` + +```ts [index.g.d.mts] +export type Notify = (msg: string) => void; + +export namespace Class { + export function getNotify(): Notify; +} +``` + +```ts [main.ts] +import { Class } from "bootsharp"; + +const notify = Class.getNotify(); +notify("hello"); +``` + +::: + +Built-in `System.Action` and `System.Func` variants are supported as well: + +::: code-group + +```csharp [Class.cs] +public class Class +{ + [Export] public static Action? Logger { get; set; } +} +``` + +```ts [index.g.d.mts] +export namespace Class { + export let logger: system.Action | undefined; +} +``` + +```ts [main.ts] +import { Class } from "bootsharp"; + +Class.logger = msg => console.log(msg); +Class.logger("hello"); ``` ::: @@ -213,8 +287,6 @@ When an inspected assembly has XML documentation generated, Bootsharp mirrors th ::: code-group ```csharp [MathApi.cs] -namespace Foo; - /// Math API. public class MathApi { @@ -227,7 +299,7 @@ public class MathApi } ``` -```ts [foo.g.d.mts] +```ts [index.g.d.mts] /** * Math API. */ @@ -255,36 +327,26 @@ Bootsharp uses different TypeScript nullish forms depending on where a nullable This is intentional and optimized for TypeScript ergonomics. Refer to the dedicated [nullability guide](/guide/nullability) for the full convention and examples. -## Type Crawling +## Namespaces -Bootsharp will crawl types from the interop signatures and mirror them as top-level exports of the same C# namespace's declaration module. For example, if you have a custom record with a property of another custom record implementing a custom interface, both records and the interface will be emitted: +Members declared inside a C# namespace are emitted into a module path derived from that namespace: dots become path separators and casing is lower-kebab-cased. Members without a namespace land in the default `index` module (as shown in the examples above). ::: code-group -```csharp [Foo.cs] -namespace Space; - -public interface IFoo { }; -public record Foo : IFoo; -public record Bar (Foo foo); +```csharp [Class.cs] +namespace Foo.Bar; -public partial class Holder +public class Class { - [Import] - public static partial Bar GetBar(); + [Export] + public static void Baz () { } } ``` -```ts [space.g.d.mts] -export interface IFoo {} -export type Foo = IFoo & Readonly<{}>; -export type Bar = Readonly<{ - foo: Foo; -}>; +```ts [main.ts] +import { Class } from "bootsharp/foo/bar"; -export namespace Holder { - export function getBar(): Bar; -} +Class.baz(); ``` ::: diff --git a/src/cs/Bootsharp.Common.Test/InstancesTest.cs b/src/cs/Bootsharp.Common.Test/InstancesTest.cs index 3bc1c3ef..fc612ac6 100644 --- a/src/cs/Bootsharp.Common.Test/InstancesTest.cs +++ b/src/cs/Bootsharp.Common.Test/InstancesTest.cs @@ -10,6 +10,11 @@ private class Foo : IFoo; private class Bar : IBar; private class Proxy (int id) : JSProxy(id); + private class DelegateProxy (int id) : JSProxy(id) + { + public void Invoke () { } + } + [Fact] public void CanExportAndDisposeInstance () { @@ -38,6 +43,18 @@ public void ShortCircuitsImportedProxies () Assert.Equal(42, Export(new Proxy(42))); } + [Fact] + public void ShortCircuitsImportedDelegates () + { + Assert.Equal(42, Export(new DelegateProxy(42).Invoke)); + } + + [Fact] + public void ExportsZeroWhenInstanceIsNull () + { + Assert.Equal(0, Export(default(object))); + } + [Fact] public void InvokesExportFactoryCallbacks () { @@ -86,4 +103,10 @@ public void ShortCircuitsImportedExports () var exported = new object(); Assert.Same(exported, Resolve(Export(exported))); } + + [Fact] + public void ImportsNullWhenInstanceIsZero () + { + Assert.Null(Resolve(0)); + } } diff --git a/src/cs/Bootsharp.Common/Interop/Instances.cs b/src/cs/Bootsharp.Common/Interop/Instances.cs index a5485e1c..791f0981 100644 --- a/src/cs/Bootsharp.Common/Interop/Instances.cs +++ b/src/cs/Bootsharp.Common/Interop/Instances.cs @@ -25,7 +25,7 @@ public static class Instances private static readonly Dictionary idByExported = new(ReferenceEqualityComparer.Instance); private static readonly Dictionary onDisposeById = []; private static readonly Queue idPool = []; - private static int nextId = int.MinValue; // C# IDs are always negative; JS's — positive. + private static int nextId = int.MinValue; // C# IDs are negative; JS's — positive; 0 reserved for null. /// /// Resolves a registered instance associated with the specified ID, or uses a factory that @@ -33,6 +33,7 @@ public static class Instances /// public static T Resolve (int id) where T : class { + if (id == 0) return null!; if (id < 0) return (T)exportedById[id]; if (importedById.GetValueOrDefault(id) is { } weak) return (T)weak.Target!; var instance = (T)importers[typeof(T)](id); @@ -47,9 +48,11 @@ public static T Resolve (int id) where T : class /// The instance to register. /// Callback to invoke when registering and disposing the instance. /// Unique ID associated with the registered instance. - public static int Export (T instance, ExportCallback? cb = null) where T : class + public static int Export (T? instance, ExportCallback? cb = null) where T : class { - if (instance is JSProxy imported) return imported._id; + if (instance is null) return 0; + if (instance is JSProxy imp) return imp._id; + if (instance is Delegate { Target: JSProxy del }) return del._id; if (idByExported.TryGetValue(instance, out var id)) return id; id = idPool.Count > 0 ? idPool.Dequeue() : nextId++; exportedById[idByExported[instance] = id] = instance; diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs similarity index 58% rename from src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs index 3813e473..34b0e617 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class InstancesTest : GenerateCSTest +public class CSInstanceTest : GenerateCSTest { protected override string TestedContent => GeneratedInstances; @@ -159,4 +159,83 @@ public interface IModule { IInstanced Item { get; set; } } Execute(); Contains("public class JS_Import_IInstanced (int id) : global::Bootsharp.JSProxy(id), global::IInstanced"); } + + [Fact] + public void GeneratesProxyForImportedDelegates () + { + AddAssembly(With( + """ + public delegate void Notify (string msg); + + public class Class + { + [Import] public static System.Action GetAction () => default!; + [Import] public static System.Func GetFunc () => default!; + [Import] public static Notify GetNotify () => default!; + } + """)); + Execute(); + Contains("Instances.RegisterImport(typeof(global::System.Action), static id => new global::System.Action(new global::Bootsharp.Generated.JS_Import_System_Action(id).Invoke));"); + Contains("Instances.RegisterImport(typeof(global::System.Func), static id => new global::System.Func(new global::Bootsharp.Generated.JS_Import_System_Func_Of_System_Int32_And_System_String(id).Invoke));"); + Contains("Instances.RegisterImport(typeof(global::Notify), static id => new global::Notify(new global::Bootsharp.Generated.JS_Import_Notify(id).Invoke));"); + Contains( + """ + public sealed class JS_Import_System_Action (int id) : global::Bootsharp.JSProxy(id) + { + ~JS_Import_System_Action() => Instances.DisposeImported(_id); + + public void Invoke () => global::Bootsharp.Generated.Interop.JS_Import_System_Action_Invoke(_id); + } + """); + Contains( + """ + public sealed class JS_Import_System_Func_Of_System_Int32_And_System_String (int id) : global::Bootsharp.JSProxy(id) + { + ~JS_Import_System_Func_Of_System_Int32_And_System_String() => Instances.DisposeImported(_id); + + public global::System.String? Invoke (global::System.Int32 arg) => global::Bootsharp.Generated.Interop.JS_Import_System_Func_Of_System_Int32_And_System_String_Invoke(_id, arg); + } + """); + Contains( + """ + public sealed class JS_Import_Notify (int id) : global::Bootsharp.JSProxy(id) + { + ~JS_Import_Notify() => Instances.DisposeImported(_id); + + public void Invoke (global::System.String msg) => global::Bootsharp.Generated.Interop.JS_Import_Notify_Invoke(_id, msg); + } + """); + } + + [Fact] + public void DoesNotGenerateProxyForExportedDelegates () + { + AddAssembly(WithClass("[Export] public static Action GetAction () => default!;")); + Execute(); + DoesNotContain("Invoke () =>"); + } + + [Fact] + public void ReclassifiesImportedClassesAsExports () + { + // it's impossible to import a concrete C# class, so it's either a user error in the authored interop + // surface or the intention is to pass back previously exported instance — we assume the latter in the + // implementation and reclassify to export direction in such cases + AddAssembly(With( + """ + public class Exported; + + public class Class + { + [Export] // the idea is that user may pass the result of a previous CreateExported(null) call + public static Exported CreateExported (Func factory = null) + { + return factory?.Invoke() ?? new Exported(); + } + } + """)); + Execute(); + DoesNotContain("JS_Import_Exported"); + DoesNotContain("RegisterImport(typeof(global::Exported)"); + } } diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs similarity index 99% rename from src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs index baecba11..918fc314 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class InteropTest : GenerateCSTest +public class CSInteropTest : GenerateCSTest { protected override string TestedContent => GeneratedInterop; diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSModuleTest.cs similarity index 99% rename from src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/CSModuleTest.cs index 54bc6507..4a030e46 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSModuleTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class ModulesTest : GenerateCSTest +public class CSModuleTest : GenerateCSTest { protected override string TestedContent => GeneratedModules; diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSSerializerTest.cs similarity index 99% rename from src/cs/Bootsharp.Publish.Test/GenerateCS/SerializerTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/CSSerializerTest.cs index 8fc3f960..56d2d800 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSSerializerTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class SerializerTest : GenerateCSTest +public class CSSerializerTest : GenerateCSTest { protected override string TestedContent => GeneratedSerializer; diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index b90dab78..bb803de1 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -145,14 +145,14 @@ public void EventPropertiesAreExportedForStaticEvents () { AddAssembly( WithClass("Foo", "[Export] public static event Action? ExpEvt;"), - WithClass("Foo", "[Export] public static event Action? Evt;"), + WithClass("Foo", "[Export] public static event Action? Evt;"), WithClass("Foo", "[Import] public static event Action? ImpEvt;")); Execute(); Contains("foo.g.d.mts", """ export namespace Class { export const expEvt: Event<[]>; - export const evt: Event<[obj: string]>; + export const evt: Event<[obj: string | undefined]>; export const impEvt: Event<[arg1: number, arg2: boolean | undefined]>; } """); @@ -357,10 +357,10 @@ public void OtherTypesAreTranslatedToAny () public void GeneratedForObjectType () { AddAssembly( - With("n", "public class Foo { public string S { get; set; } public int I { get; set; } }"), - WithClass("n", "[Export] public static Foo Method (Foo t) => default;")); + With("public class Foo { public string S { get; set; } public int I { get; set; } }"), + WithClass("[Export] public static Foo Method (Foo t) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function method(t: Foo): Foo; @@ -376,12 +376,12 @@ export interface Foo { public void GeneratedForInterfaceAndImplementation () { AddAssembly( - With("n", "public interface Interface { Interface Foo { get; } void Bar (Interface b); }"), - With("n", "public class Base { }"), - With("n", "public class Derived : Base, Interface { public Interface Foo { get; } public void Bar (Interface b) {} }"), - WithClass("n", "[Export] public static Derived Method (Base b) => default;")); + With("public interface Interface { Interface Foo { get; } void Bar (Interface b); }"), + With("public class Base { }"), + With("public class Derived : Base, Interface { public Interface Foo { get; } public void Bar (Interface b) {} }"), + WithClass("[Export] public static Derived Method (Base b) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function method(b: Base): Derived; @@ -403,11 +403,11 @@ export interface Interface { public void GeneratedForTypeWithListProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public List Items { get; } }"), - WithClass("n", "[Export] public static Container Combine (List items) => default;")); + With("public interface Item { }"), + With("public class Container { public List Items { get; } }"), + WithClass("[Export] public static Container Combine (List items) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function combine(items: Array): Container; @@ -424,11 +424,11 @@ export interface Container { public void GeneratedForTypeWithJaggedArrayProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public Item[][] Items { get; } }"), - WithClass("n", "[Export] public static Container Get () => default;")); + With("public interface Item { }"), + With("public class Container { public Item[][] Items { get; } }"), + WithClass("[Export] public static Container Get () => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function get(): Container; @@ -445,11 +445,11 @@ export interface Item { public void GeneratedForTypeWithReadOnlyListProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public IReadOnlyList Items { get; } }"), - WithClass("n", "[Export] public static Container Combine (IReadOnlyList items) => default;")); + With("public interface Item { }"), + With("public class Container { public IReadOnlyList Items { get; } }"), + WithClass("[Export] public static Container Combine (IReadOnlyList items) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function combine(items: Array): Container; @@ -466,11 +466,11 @@ export interface Container { public void GeneratedForTypeWithDictionaryProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public Dictionary Items { get; } }"), - WithClass("n", "[Export] public static Container Combine (Dictionary items) => default;")); + With("public interface Item { }"), + With("public class Container { public Dictionary Items { get; } }"), + WithClass("[Export] public static Container Combine (Dictionary items) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function combine(items: Map): Container; @@ -487,11 +487,11 @@ export interface Container { public void GeneratedForTypeWithReadOnlyDictionaryProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public IReadOnlyDictionary Items { get; } }"), - WithClass("n", "[Export] public static Container Combine (IReadOnlyDictionary items) => default;")); + With("public interface Item { }"), + With("public class Container { public IReadOnlyDictionary Items { get; } }"), + WithClass("[Export] public static Container Combine (IReadOnlyDictionary items) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function combine(items: Map): Container; @@ -508,11 +508,11 @@ export interface Container { public void GeneratedForTypeWithCollectionProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public ICollection Items { get; } }"), - WithClass("n", "[Export] public static Container Combine (ICollection items) => default;")); + With("public interface Item { }"), + With("public class Container { public ICollection Items { get; } }"), + WithClass("[Export] public static Container Combine (ICollection items) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function combine(items: Array): Container; @@ -529,11 +529,11 @@ export interface Container { public void GeneratedForTypeWithReadOnlyCollectionProperty () { AddAssembly( - With("n", "public interface Item { }"), - With("n", "public class Container { public IReadOnlyCollection Items { get; } }"), - WithClass("n", "[Export] public static Container Combine (IReadOnlyCollection items) => default;")); + With("public interface Item { }"), + With("public class Container { public IReadOnlyCollection Items { get; } }"), + WithClass("[Export] public static Container Combine (IReadOnlyCollection items) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function combine(items: Array): Container; @@ -550,11 +550,11 @@ export interface Container { public void GeneratedForGenericClass () { AddAssembly( - With("n", "public class Generic where T: notnull { public required T Value { get; set; } }"), - With("n", "public class GenericNull { public T? Value { get; } public T? Foo (T? t) => default; }"), - WithClass("n", "[Export] public static void Method (Generic a, GenericNull b) { }")); + With("public class Generic where T: notnull { public required T Value { get; set; } }"), + With("public class GenericNull { public T? Value { get; } public T? Foo (T? t) => default; }"), + WithClass("[Export] public static void Method (Generic a, GenericNull b) { }")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function method(a: Generic, b: GenericNull): void; @@ -573,11 +573,11 @@ export interface GenericNull { public void GeneratedForGenericRecord () { AddAssembly( - With("n", "public record Generic where T: notnull { public T Value { get; set; } }"), - With("n", "public record GenericNull { public T? Value { get; set; } }"), - WithClass("n", "[Export] public static void Method (Generic a, GenericNull b) { }")); + With("public record Generic where T: notnull { public T Value { get; set; } }"), + With("public record GenericNull { public T? Value { get; set; } }"), + WithClass("[Export] public static void Method (Generic a, GenericNull b) { }")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function method(a: Generic, b: GenericNull): void; @@ -595,10 +595,10 @@ export namespace Class { public void GeneratedForGenericInterface () { AddAssembly( - With("n", "public interface IGenericInterface { public T Value { get; set; } }"), - WithClass("n", "[Export] public static IGenericInterface Method () => default;")); + With("public interface IGenericInterface { public T Value { get; set; } }"), + WithClass("[Export] public static IGenericInterface Method () => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function method(): IGenericInterface; @@ -615,9 +615,9 @@ public void GeneratedForNestedGenericTypes () AddAssembly( With("Foo", "public class GenericClass { public T Value { get; set; } }"), With("Bar", "public interface GenericInterface { public T Value { get; set; } }"), - WithClass("n", "[Export] public static void Method (Foo.GenericClass> p) { }")); + WithClass("[Export] public static void Method (Foo.GenericClass> p) { }")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function method(p: foo.GenericClass>): void; @@ -629,21 +629,65 @@ export namespace Class { public void GeneratedForGenericClassWithMultipleTypeArguments () { AddAssembly( - With("n", "public class GenericClass { public T1 Key { get; set; } public T2 Value { get; set; } }"), - WithClass("n", "[Export] public static void Method (GenericClass p) { }")); + With("public class GenericClass { public T1 Key { get; set; } public T2 Value { get; set; } }"), + WithClass("[Export] public static void Method (GenericClass p) { }")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { - export function method(p: GenericClass): void; + export function method(p: GenericClass2): void; } - export interface GenericClass { + export interface GenericClass2 { key?: T1; value?: T2; } """); } + [Fact] + public void GeneratesForDelegates () + { + AddAssembly( + With("public record Payload;"), + With("public delegate void Notify (string? msg, int? num);"), + WithClass( + """ + [Export] public static Func GetFunc () => default!; + [Export] public static Action? GetAction () => default!; + [Import] public static Func GetParse () => default!; + [Export] public static Notify GetNotify () => default!; + """)); + Execute(); + Contains( + """ + export namespace Class { + export function getFunc(): system.Func3; + export function getAction(): system.Action | null; + export let getParse: () => system.Func3; + export function getNotify(): Notify; + } + """); + Contains("export type Notify = (msg: string | undefined, num: number | undefined) => void;"); + Contains("system.g.d.mts", "export type Func3 = (arg1: T1, arg2: T2) => TResult;"); + Contains("system.g.d.mts", "export type Action = (obj: T) => void;"); + } + + [Fact] + public void NullabilityPropagatesAcrossNestedArgs () + { + AddAssembly( + With("public class Foo { }"), + With("public interface IBar { }"), + WithClass( + """ + [Export] public static Func GetFunc () => default!; + [Export] public static Func> GetNested () => default!; + """)); + Execute(); + Contains("export function getFunc(): system.Func2;"); + Contains("export function getNested(): system.Func2>;"); + } + [Fact] public void DoesNotDuplicateGenerics () { @@ -1061,6 +1105,17 @@ public void NullableMethodArgumentsUnionWithUndefined () Contains("export let fun: (nya: number | undefined) => void;"); } + [Fact] + public void DefaultMethodArgumentsAreOptional () + { + AddAssembly( + WithClass("[Export] public static void Foo (string bar = \"\", int? nya = null) { }"), + WithClass("[Import] public static void Fun (string bar = \"\", int? nya = null) { }")); + Execute(); + Contains("export function foo(bar?: string, nya?: number): void;"); + Contains("export let fun: (bar?: string, nya?: number) => void;"); + } + [Fact] public void NullableMethodReturnTypesUnionWithNull () { @@ -1120,11 +1175,11 @@ public void NullableDictionaryValueTypesUnionWithNull () public void NullablePropertiesHaveOptionalModificator () { AddAssembly( - With("n", "public class Foo { public bool? Bool { get; } }"), - With("n", "public class Bar { public Foo? Foo { get; } }"), - WithClass("n", "[Export] public static Foo FooBar (Bar bar) => default;")); + With("public class Foo { public bool? Bool { get; } }"), + With("public class Bar { public Foo? Foo { get; } }"), + WithClass("[Export] public static Foo FooBar (Bar bar) => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function fooBar(bar: Bar): Foo; @@ -1142,11 +1197,11 @@ export interface Foo { public void NullableEnumsAreCrawled () { AddAssembly( - With("n", "public enum Foo { A, B }"), - With("n", "public class Bar { public Foo? Foo { get; } }"), - WithClass("n", "[Export] public static Bar GetBar () => default;")); + With("public enum Foo { A, B }"), + With("public class Bar { public Foo? Foo { get; } }"), + WithClass("[Export] public static Bar GetBar () => default;")); Execute(); - Contains("n.g.d.mts", + Contains( """ export namespace Class { export function getBar(): Bar; diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs index 0814ec7c..7b5e7147 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs @@ -162,6 +162,61 @@ public partial class Class """); } + [Fact] + public void EmitsForExportedDelegates () + { + AddAssembly(With( + """ + public delegate void Notify (string msg); + + public class Class + { + [Export] public static System.Action GetAction () => default!; + [Export] public static System.Func GetFunc () => default!; + [Export] public static Notify GetNotify () => default!; + } + """)); + Execute(); + Contains( + """ + $i.System_Action = class JS_Export_System_Action { + constructor(_id) { + const fn = () => system.Action.invoke(_id); + fn._id = _id; + return fn; + } + }; + """); + Contains( + """ + $i.System_Func_Of_System_Int32_And_System_String = class JS_Export_System_Func_Of_System_Int32_And_System_String { + constructor(_id) { + const fn = (arg) => system.Func_Of_Int32_And_String.invoke(_id, arg); + fn._id = _id; + return fn; + } + }; + """); + Contains( + """ + $i.Notify = class JS_Export_Notify { + constructor(_id) { + const fn = (msg) => index.Notify.invoke(_id, msg); + fn._id = _id; + return fn; + } + }; + """); + } + + [Fact] + public void DoesNotEmitForImportedDelegate () + { + AddAssembly(WithClass("[Import] public static Action GetAction () => default!;")); + Execute(); + DoesNotContain("invoke"); + } + [Fact] public void DoesNotEmitDuplicateSpecializedImporters () { diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 3be5a835..fda0458c 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -91,13 +91,15 @@ public static NullabilityInfo GetNullity (EventInfo evt, ParameterInfo param) return GetNullity(param); } + public static bool IsNullable (NullabilityInfo? info) => info?.ReadState == NullabilityState.Nullable; public static bool IsNullable (Type type, NullabilityInfo? info) => IsNullable(type, info, out _); public static bool IsNullable (Type type, [NotNullWhen(true)] out Type? value) => IsNullable(type, null, out value); public static bool IsNullable (Type type, NullabilityInfo? info, [NotNullWhen(true)] out Type? value) { - if (info?.ReadState == NullabilityState.Nullable) value = type; - else if (type.IsGenericType && type.Name.Contains("Nullable`") && type.GenericTypeArguments.Length == 1) + if (type.IsGenericType && type.Name.Contains("Nullable`") && type.GenericTypeArguments.Length == 1) value = type.GenericTypeArguments[0]; + else if (IsNullable(info) && (!type.IsGenericTypeParameter || IsUserType(type))) + value = type; else value = null; return value != null; } @@ -123,7 +125,7 @@ public static string BuildId (Type type, bool full = true, char separator = '_') public static string BuildSyntax (Type type, NullabilityInfo? nul = null, bool forceNil = false, bool full = true) { - var nil = (forceNil || nul?.ReadState == NullabilityState.Nullable) ? "?" : ""; + var nil = (forceNil || IsNullable(nul)) ? "?" : ""; var global = full ? "global::" : ""; if (IsVoid(type)) return "void"; if (type.IsArray) return $"{BuildSyntax(type.GetElementType()!, nul?.ElementType)}[]{nil}"; diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs index 10ebe533..30cdaaf7 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace Bootsharp.Publish; /// @@ -59,6 +61,18 @@ internal record InstanceMeta (Type Clr) : ProxyMeta(Clr) public string? Importer { get; init; } } +/// +/// Describes an instance surface projected from a delegate type. +/// +internal sealed record DelegateMeta (Type Clr) : InstanceMeta(Clr) +{ + /// + /// Describes the "Invoke" method of the delegate. + /// + public MethodMeta Invoker => (MethodMeta)Members.First(); + protected override bool PrintMembers (StringBuilder builder) => base.PrintMembers(builder); // w/a C# bug +} + /// /// Describes the generated proxy used by . /// diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index 040d2772..15e36d30 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -71,9 +71,11 @@ public IReadOnlyCollection Collect () { if (its.TryGetValue((type, ik), out var it)) return it; if (IsTaskWithResult(type, out var result)) return InspectInstance(result, ik); + if (IsDelegate(type)) return its[(type, ik)] = InspectDelegate(type, ik); if (!IsInstanced(type)) return null; - // instances with events need specialized registrars to un-/sub them - var special = type.GetEvents().Length > 0; + if (ik == InteropKind.Import && !type.IsInterface) // likely passing back an exported instance — reclassify + return InspectInstance(type, InteropKind.Export)!; + var special = type.GetEvents().Length > 0; // instances with events need specialized registrars to un-/sub it = its[(type, ik)] = new(type) { IK = ik, Proxy = BuildProxy(type, ik), @@ -85,7 +87,6 @@ public IReadOnlyCollection Collect () static bool IsInstanced (Type type) { - if (IsDelegate(type)) return true; // Instanced types are mutable user types that are passed by reference when crossing the // interop boundary (as opposed to serialized immutable types, which are copied by value). if (!IsUserType(type)) return false; @@ -94,6 +95,14 @@ static bool IsInstanced (Type type) } } + private DelegateMeta InspectDelegate (Type type, InteropKind ik) + { + var members = new List(); + var del = new DelegateMeta(type) { IK = ik, Proxy = BuildProxy(type, ik), Members = members }; + members.Add(InspectMethod(type.GetMethod("Invoke")!, ik, del)); + return del; + } + private T InspectMembers (T surf, InteropKind ik) where T : SurfaceMeta { var members = (ICollection)surf.Members; diff --git a/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs similarity index 74% rename from src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs index ac1f7d32..31f055c4 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs @@ -1,9 +1,10 @@ namespace Bootsharp.Publish; /// -/// Generates binding proxies for imported instances and instance-specific export handlers. +/// Generates C# binding proxies for . +/// Symmetrical to , which generates the same but for the JS side. /// -internal sealed class InstanceGenerator +internal sealed class CSInstanceGenerator { private InstanceMeta it = null!; @@ -19,7 +20,7 @@ namespace Bootsharp.Generated; public static partial class Instances { - internal static int Export (T it, Bootsharp.Instances.ExportCallback? cb = null) where T : class => Bootsharp.Instances.Export(it, cb); + internal static int Export (T? it, Bootsharp.Instances.ExportCallback? cb = null) where T : class => Bootsharp.Instances.Export(it, cb); internal static T Exported (int id) where T : class => Bootsharp.Instances.Exported(id); internal static T Resolve (int id) where T : class => Bootsharp.Instances.Resolve(id); @@ -46,7 +47,9 @@ internal static void RegisterImports () private static string EmitImporter (InstanceMeta it) { - var proxy = $"static id => new {it.Proxy.Syntax}(id)"; + var proxy = it is DelegateMeta + ? $"static id => new {it.Syntax}(new {it.Proxy.Syntax}(id).Invoke)" + : $"static id => new {it.Proxy.Syntax}(id)"; return $"Bootsharp.Instances.RegisterImport(typeof({it.Syntax}), {proxy});"; } @@ -71,9 +74,14 @@ private static string EmitExporter (InstanceMeta it) """; } - private string EmitProxy (InstanceMeta it) => + private string EmitProxy (InstanceMeta it) => (this.it = it) switch { + DelegateMeta del => EmitDelegateProxy(del), + _ => EmitOpaqueProxy(it) + }; + + private string EmitOpaqueProxy (InstanceMeta it) => $$""" - public class {{(this.it = it).Proxy.Id}} (int id) : global::Bootsharp.JSProxy(id), {{it.Syntax}} + public class {{it.Proxy.Id}} (int id) : global::Bootsharp.JSProxy(id), {{it.Syntax}} { ~{{it.Proxy.Id}}() => Instances.DisposeImported(_id); @@ -81,6 +89,23 @@ public class {{(this.it = it).Proxy.Id}} (int id) : global::Bootsharp.JSProxy(id } """; + private static string EmitDelegateProxy (DelegateMeta del) + { + var inv = del.Invoker; + var args = string.Join(", ", inv.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = PrependIdArg(string.Join(", ", inv.Args.Select(a => a.Name))); + var fn = $"global::Bootsharp.Generated.Interop.{del.Proxy.Id}_{inv.Name}"; + return + $$""" + public sealed class {{del.Proxy.Id}} (int id) : global::Bootsharp.JSProxy(id) + { + ~{{del.Proxy.Id}}() => Instances.DisposeImported(_id); + + public {{inv.Return.TypeSyntax}} Invoke ({{args}}) => {{fn}}({{callArgs}}); + } + """; + } + private string EmitMemberImport (MemberMeta member) => member switch { EventMeta evt => EmitEventImport(evt), PropertyMeta prop => EmitPropertyImport(prop), diff --git a/src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs similarity index 97% rename from src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs index 34a62ccc..0d8139e6 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs @@ -4,8 +4,9 @@ namespace Bootsharp.Publish; /// /// Generates bindings to be picked by .NET's interop source generator. +/// Symmetrical to , which generates the same but for the JS side. /// -internal sealed class InteropGenerator +internal sealed class CSInteropGenerator { [MemberNotNullWhen(true, nameof(it))] private bool isIt => srf is InstanceMeta; @@ -200,9 +201,8 @@ private string BuildParameter (ValueMeta value, string name) private string BuildValueSyntax (ValueMeta value) { - var nil = value.Nullable && !value.IsSerialized ? "?" : ""; - if (value.IsInstanced) return $"int{nil}"; - if (value.IsSerialized) return $"long{nil}"; + if (value.IsInstanced) return "int"; + if (value.IsSerialized) return "long"; return value.TypeSyntax; } diff --git a/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs similarity index 97% rename from src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs index d9c81e9c..3ef65f31 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs @@ -1,9 +1,9 @@ namespace Bootsharp.Publish; /// -/// Generates implementations for interop modules. +/// Generates C# implementations for . /// -internal sealed class ModuleGenerator +internal sealed class CSModuleGenerator { private ModuleMeta md = null!; diff --git a/src/cs/Bootsharp.Publish/GenerateCS/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSSerializerGenerator.cs similarity index 95% rename from src/cs/Bootsharp.Publish/GenerateCS/SerializerGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/CSSerializerGenerator.cs index f5401fac..ab2db518 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSSerializerGenerator.cs @@ -1,6 +1,10 @@ namespace Bootsharp.Publish; -internal sealed class SerializerGenerator +/// +/// Generates serializers for the C# side. +/// Symmetrical to , which generates the same but for the JS side. +/// +internal sealed class CSSerializerGenerator { public string Generate (IReadOnlyCollection srd) => srd.Count == 0 ? "" : $$""" diff --git a/src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs b/src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs index 59acb866..661c027a 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs @@ -34,7 +34,7 @@ private SolutionInspection InspectSolution () private void GenerateSerializer (SolutionInspection spec) { - var generator = new SerializerGenerator(); + var generator = new CSSerializerGenerator(); var serialized = spec.Types.OfType().ToArray(); var content = generator.Generate(serialized); WriteGenerated(SerializerFilePath, content); @@ -42,7 +42,7 @@ private void GenerateSerializer (SolutionInspection spec) private void GenerateInstances (SolutionInspection spec) { - var generator = new InstanceGenerator(); + var generator = new CSInstanceGenerator(); var instanced = spec.Types.OfType().ToArray(); var content = generator.Generate(instanced); WriteGenerated(InstancesFilePath, content); @@ -50,7 +50,7 @@ private void GenerateInstances (SolutionInspection spec) private void GenerateModules (SolutionInspection spec) { - var generator = new ModuleGenerator(); + var generator = new CSModuleGenerator(); var mds = spec.Types.OfType().ToArray(); var content = generator.Generate(mds); WriteGenerated(ModulesFilePath, content); @@ -58,7 +58,7 @@ private void GenerateModules (SolutionInspection spec) private void GenerateInterop (SolutionInspection spec) { - var generator = new InteropGenerator(); + var generator = new CSInteropGenerator(); var surfaces = spec.Types.OfType().ToArray(); var content = generator.Generate(surfaces); WriteGenerated(InteropFilePath, content); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs index 9f6d7fd0..b56ac54e 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -2,6 +2,9 @@ namespace Bootsharp.Publish; +/// +/// Generates TypeScript type declarations. +/// internal sealed class DeclarationGenerator { private readonly HashSet declared = []; @@ -50,6 +53,7 @@ private void DeclareNode (JSNode node) if (declared.Add(type.Clr.IsGenericType ? type.Clr.GetGenericTypeDefinition() : type.Clr)) if (type is SerializedEnumMeta enu) DeclareEnum(enu); else if (type is SerializedObjectMeta o) DeclareSerialized(o); + else if (type is DelegateMeta d) DeclareDelegate(d); else if (type is InstanceMeta it) DeclareInstance(it); else if (type is SurfaceMeta srf) DeclareSurface(srf); foreach (var child in node.Children) @@ -83,6 +87,14 @@ private void DeclareSerialized (SerializedObjectMeta obj) bld.Exit("}>;"); } + private void DeclareDelegate (DelegateMeta del) + { + doc.Type(del); + var inv = del.Invoker; + var args = string.Join(", ", inv.Args.Select(a => $"{a.JSName}{ts.BuildArg(a.Info)}")); + bld.Line($"export type {ts.BuildName(del.Clr)} = ({args}) => {ts.BuildReturn(inv.Info)};"); + } + private void DeclareInstance (InstanceMeta it) { doc.Type(it); @@ -103,7 +115,7 @@ string BuildExtensions () void DeclareEvent (EventMeta evt) { doc.Event(evt); - var args = string.Join(", ", evt.Args.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); + var args = string.Join(", ", evt.Args.Select(a => $"{a.JSName}{ts.BuildArg(evt.Info, a.Info)}")); bld.Line($"{evt.JSName}: Event<[{args}]>;"); } @@ -117,7 +129,7 @@ void DeclareProperty (PropertyMeta prop) void DeclareMethod (MethodMeta method) { doc.Method(method); - var args = string.Join(", ", method.Args.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); + var args = string.Join(", ", method.Args.Select(a => $"{a.JSName}{ts.BuildArg(a.Info)}")); bld.Line($"{method.JSName}({args}): {ts.BuildReturn(method.Info)};"); } } @@ -132,7 +144,7 @@ private void DeclareSurface (SurfaceMeta surf) void DeclareEvent (EventMeta evt) { doc.Event(evt); - var args = string.Join(", ", evt.Args.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); + var args = string.Join(", ", evt.Args.Select(a => $"{a.JSName}{ts.BuildArg(evt.Info, a.Info)}")); bld.Line($"export const {evt.JSName}: Event<[{args}]>;"); } @@ -152,7 +164,7 @@ void DeclareProperty (PropertyMeta prop) void DeclareMethod (MethodMeta method) { doc.Method(method); - var args = string.Join(", ", method.Args.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); + var args = string.Join(", ", method.Args.Select(a => $"{a.JSName}{ts.BuildArg(a.Info)}")); var result = ts.BuildReturn(method.Info); if (method.IK == InteropKind.Export) bld.Line($"export function {method.JSName}({args}): {result};"); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs index e34bf632..5f491331 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs @@ -4,6 +4,9 @@ namespace Bootsharp.Publish; +/// +/// Generates JSDoc annotations for the TypeScript type declarations. +/// internal sealed class DocumentationBuilder { private readonly Dictionary<(string Assembly, string Key), XElement> xmlByKey = []; diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs index 520ac1ee..3b5a5171 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs @@ -2,10 +2,12 @@ namespace Bootsharp.Publish; +/// +/// Builds TypeScript type syntax. +/// internal sealed class TypeSyntaxBuilder (JSModules mds) { private JSModule module = null!; - private NullabilityInfo? nullity; public void EnterModule (JSModule module) { @@ -31,15 +33,16 @@ public string BuildArg (ParameterInfo param) param = param.Member.DeclaringType.GetGenericTypeDefinition() .GetMethod(param.Member.Name)!.GetParameters()[param.Position]; var nul = GetNullity(param); + if (param.HasDefaultValue) return $"?: {Build(param.ParameterType, nul)}"; var post = IsNullable(param.ParameterType, nul) ? " | undefined" : ""; - return Build(param.ParameterType, nul) + post; + return $": {Build(param.ParameterType, nul)}{post}"; } public string BuildArg (EventInfo evt, ParameterInfo param) { var nul = GetNullity(evt, param); var post = IsNullable(param.ParameterType, nul) ? " | undefined" : ""; - return Build(param.ParameterType, nul) + post; + return $": {Build(param.ParameterType, nul)}{post}"; } public string BuildReturn (MethodInfo method) @@ -67,42 +70,30 @@ public string BuildVariable (PropertyInfo prop) return Build(prop.PropertyType, nul) + post; } - private string Build (Type type, NullabilityInfo? nullity) - { - this.nullity = nullity; - // nullability of topmost declarations is handled downstream (?/undefined/null) - if (IsNullable(type, nullity, out var value)) type = value; - return Build(type); - } - - private string Build (Type type) + private string Build (Type type, NullabilityInfo? nul) { if (type.IsGenericTypeParameter) return type.Name; - if (IsNullable(type, out var nullValue)) return BuildNullable(nullValue); - if (IsTaskLike(type)) return BuildTask(type); - if (IsList(type, out var element)) return BuildList(type, element); - if (IsDictionary(type, out var key, out var value)) return BuildDictionary(key, value); - if (IsUserType(type)) return BuildUser(type); + if (IsNullable(type, out var inner)) return Build(inner, EnterNullity(nul)); + if (IsTaskLike(type)) return BuildTask(type, nul); + if (IsList(type, out var element)) return BuildList(type, element, nul); + if (IsDictionary(type, out var key, out var value)) return BuildDictionary(key, value, nul); + if (IsUserType(type) || IsDelegate(type)) return BuildUser(type, nul); return BuildPrimitive(type); } - private string BuildNullable (Type value) - { - EnterNullity(); - return $"{Build(value)} | null"; - } - - private string BuildTask (Type type) + private string BuildTask (Type type, NullabilityInfo? nul) { - var nil = EnterNullity() ? " | null" : ""; + nul = EnterNullity(nul); + var nil = IsNullable(nul) ? " | null" : ""; if (!IsTaskWithResult(type, out var result)) return $"Promise{nil}"; - return $"Promise<{Build(result)}{nil}>"; + return $"Promise<{Build(result, nul)}{nil}>"; } - private string BuildList (Type type, Type element) + private string BuildList (Type type, Type element, NullabilityInfo? nul) { - if (EnterNullity()) return $"Array<{Build(element)} | null>"; - if (!type.IsArray) return $"Array<{Build(element)}>"; + nul = EnterNullity(nul); + if (IsNullable(element, nul)) return $"Array<{Build(element, nul)} | null>"; + if (!type.IsArray) return $"Array<{Build(element, nul)}>"; return Type.GetTypeCode(element) switch { TypeCode.Byte => "Uint8Array", TypeCode.SByte => "Int8Array", @@ -113,24 +104,32 @@ private string BuildList (Type type, Type element) TypeCode.Int64 => "BigInt64Array", TypeCode.Single => "Float32Array", TypeCode.Double => "Float64Array", - _ => $"Array<{Build(element)}>" + _ => $"Array<{Build(element, nul)}>" }; } - private string BuildDictionary (Type key, Type value) + private string BuildDictionary (Type key, Type value, NullabilityInfo? nul) { - var nil = EnterNullity(1) ? " | null" : ""; - return $"Map<{Build(key)}, {Build(value)}{nil}>"; + nul = EnterNullity(nul, 1); + var nil = IsNullable(value, nul) ? " | null" : ""; + return $"Map<{Build(key, null)}, {Build(value, nul)}{nil}>"; } - private string BuildUser (Type type) + private string BuildUser (Type type, NullabilityInfo? nul) { var @ref = mds.Ref(type, module); if (!type.IsGenericType) return @ref; - EnterNullity(); + var argTypes = type.GetGenericArguments(); + var args = new List(argTypes.Length); + for (var i = 0; i < argTypes.Length; i++) + { + var argNul = EnterNullity(nul, i); + var nil = IsNullable(argTypes[i], argNul) ? " | undefined" : ""; + args.Add($"{Build(argTypes[i], argNul)}{nil}"); + } var name = @ref[..@ref.IndexOf("_Of_", StringComparison.Ordinal)]; - var args = string.Join(", ", type.GetGenericArguments().Select(Build)); - return $"{name}<{args}>"; + var disc = argTypes.Length > 1 ? argTypes.Length.ToString() : ""; + return $"{name}{disc}<{string.Join(", ", args)}>"; } private string BuildPrimitive (Type type) @@ -150,16 +149,10 @@ private bool IsNumber (Type type) => Type.GetTypeCode(type) is TypeCode.Byte or TypeCode.SByte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64 or TypeCode.Int16 or TypeCode.Int32 or TypeCode.Decimal or TypeCode.Double or TypeCode.Single; - private bool EnterNullity (int idx = 0) + private static NullabilityInfo? EnterNullity (NullabilityInfo? nul, int idx = 0) { - if (nullity == null) return false; - if (nullity.GenericTypeArguments.Length > idx) nullity = nullity.GenericTypeArguments[idx]; - else if (nullity.ElementType != null) nullity = nullity.ElementType; - else - { - nullity = null; - return false; - } - return nullity.ReadState == NullabilityState.Nullable; + if (nul == null) return null; + if (nul.GenericTypeArguments.Length > idx) return nul.GenericTypeArguments[idx]; + return nul.ElementType; } } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs b/src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs index cd9d5d45..8d006c22 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs @@ -4,6 +4,9 @@ namespace Bootsharp.Publish; +/// +/// Monkey-patches various internal .NET JavaScript files. +/// internal sealed class DotNetPatcher (string buildDir) { private readonly string dotnet = Path.Combine(buildDir, "dotnet.js"); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs index fff5c66a..e2d20a9d 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs @@ -1,5 +1,8 @@ namespace Bootsharp.Publish; +/// +/// Generates import bindings for .NET's JavaScript interop infra. +/// internal sealed class JSImportsGenerator { public string Generate (JSModules mds) => diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs index 9b08a7c4..4eae0ee7 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs @@ -1,5 +1,9 @@ namespace Bootsharp.Publish; +/// +/// Generates JavaScript binding proxies for . +/// Symmetrical to , which generates the same but for the C# side. +/// internal sealed class JSInstanceGenerator (bool debug, JSModules md) { public string Generate (IReadOnlyCollection its) => @@ -49,7 +53,28 @@ string EmitHandler (EventMeta e) } } - private string EmitProxy (InstanceMeta it) => + private string EmitProxy (InstanceMeta it) => it switch { + DelegateMeta del => EmitDelegateProxy(del), + _ => EmitOpaqueProxy(it) + }; + + private string EmitDelegateProxy (DelegateMeta del) + { + var inv = del.Invoker; + var args = string.Join(", ", inv.Args.Select(a => a.JSName)); + var invArgs = PrependIdArg(args); + return $$""" + $i.{{del.Id}} = class {{del.Proxy.Id}} { + constructor(_id) { + const fn = ({{args}}) => {{md.Ref(inv.Surf)}}.{{inv.JSName}}({{invArgs}}); + fn._id = _id; + return fn; + } + }; + """; + } + + private string EmitOpaqueProxy (InstanceMeta it) => $$""" $i.{{it.Id}} = class {{it.Proxy.Id}} { {{Fmt([ @@ -76,11 +101,11 @@ private string EmitEvent (EventMeta evt) private string EmitMethod (MethodMeta method) { - var sigArgs = string.Join(", ", method.Args.Select(a => a.JSName)); - var invArgs = sigArgs.Length > 0 ? $"this._id, {sigArgs}" : "this._id"; + var args = string.Join(", ", method.Args.Select(a => a.JSName)); + var invArgs = args.Length > 0 ? $"this._id, {args}" : "this._id"; var bodyExp = $"{md.Ref(method.Surf)}.{method.JSName}({invArgs})"; if (!method.Void) bodyExp = $"return {bodyExp}"; - return $"{method.JSName}({sigArgs}) {{ {bodyExp}; }}"; + return $"{method.JSName}({args}) {{ {bodyExp}; }}"; } private string EmitProperty (PropertyMeta p) => Fmt(0, diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs index c30de898..c7013f70 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs @@ -2,6 +2,10 @@ namespace Bootsharp.Publish; +/// +/// Generates JavaScript bindings under ES modules projected from the C# interop surfaces. +/// Symmetrical to , which generates the same but for the C# side. +/// internal sealed class JSModuleGenerator (bool debug) { private readonly CodeBuilder bld = new(); @@ -107,7 +111,7 @@ private void EmitPropertyExport (PropertyMeta prop) var fnName = $"{id}_Get{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var body = ExportJS(prop.Get, isIt ? $"{invName}(_id)" : $"{invName}()"); - if (prop.Get.Nullable && !prop.Get.IsInstanced) body += " ?? undefined"; + if (prop.Get.Nullable) body += " ?? undefined"; bld.Line(isIt ? $"get{prop.Name}(_id) {{ return {body}; }}" : $"get {prop.JSName}() {{ return {body}; }}"); @@ -165,9 +169,8 @@ private void EmitMethodImport (MethodMeta method) var args = string.Join(", ", method.Args.Select(a => a.JSName)); if (isIt) args = PrependIdArg(args); var invArgs = string.Join(", ", method.Args.Select(ExportJS)); - var invName = isIt - ? $"$i.imported(_id).{name}" - : $"this.{name}Handler"; + var invName = srf is DelegateMeta ? "$i.imported(_id)" + : isIt ? $"$i.imported(_id).{name}" : $"this.{name}Handler"; var bodyExp = ImportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); var srdHandler = $"{(wait ? "async " : "")}({args}) => {bodyExp}"; if (isIt) bld.Line($"{name}Serialized: {srdHandler}"); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs index 094f3995..53f3e8c7 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs @@ -17,7 +17,7 @@ internal sealed class JSModules public JSModules (IReadOnlyCollection types) { List = types - .Where(t => IsUserType(t.Clr)).GroupBy(t => t.JSModule) + .Where(t => IsUserType(t.Clr) || t is DelegateMeta).GroupBy(t => t.JSModule) .Select(g => new JSModule(g.Key, g.ToArray())).ToArray(); mdByPath = List.ToDictionary(m => m.Path); this.types = types; diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs index 6ed510eb..8e94f78e 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs @@ -1,5 +1,9 @@ namespace Bootsharp.Publish; +/// +/// Generates serializers for the JavaScript side. +/// Symmetrical to , which generates the same but for the C# side. +/// internal sealed class JSSerializerGenerator { public string Generate (IReadOnlyCollection srd) => diff --git a/src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs index e698f457..c368dc0a 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs @@ -1,5 +1,8 @@ namespace Bootsharp.Publish; +/// +/// Generates a manifest listing resources required to initialize the .NET runtime. +/// internal sealed class ResourceGenerator (string entryAssemblyName, bool debug, bool g11n) { private readonly List assemblies = []; diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 8536be22..412582df 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.331 + 0.8.0-alpha.353 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/src/instances.mts b/src/js/src/instances.mts index a7894eba..8da30a71 100644 --- a/src/js/src/instances.mts +++ b/src/js/src/instances.mts @@ -6,13 +6,14 @@ const importedById = new Map(); const idByImported = new Map(); const onDisposeById = new Map void>(); const idPool = new Array(); -let nextId = 0; // JS IDs are always positive; C#'s — negative. +let nextId = 1; // JS IDs are positive; C#'s — negative; 0 reserved for null. export const instances = { /** Resolves a registered instance associated with the specified ID, * or uses the specified factory to register a new exported instance. */ - resolve(id: number, factory: new (id: number) => T): T { - if (id >= 0) return importedById.get(id) as T; + resolve(id: number, factory: new (id: number) => T): T | null { + if (id === 0) return null; + if (id > 0) return importedById.get(id) as T; const exported = exportedById.get(id)?.deref() as T; if (exported != null) return exported; const proxy = new factory(id); @@ -22,7 +23,8 @@ export const instances = { }, /** Registers specified imported (JS) instance and returns the associated unique ID. * Short-circuits already registered imported and exported instances. */ - import(instance: object, cb?: (id: number) => () => void): number { + import(instance?: object, cb?: (id: number) => () => void): number { + if (instance == null) return 0; const exportedId = (instance as { _id: number })?._id; if (exportedId !== undefined) return exportedId; const importedId = idByImported.get(instance); diff --git a/src/js/test/cs/Test.Library/Assertions.cs b/src/js/test/cs/Test.Library/Assertions.cs index 64c12613..8dd4129e 100644 --- a/src/js/test/cs/Test.Library/Assertions.cs +++ b/src/js/test/cs/Test.Library/Assertions.cs @@ -1,12 +1,13 @@ global using static Test.Library.Assertions; using System; +using System.Runtime.CompilerServices; namespace Test.Library; public static class Assertions { - public static void Assert (bool condition) + public static void Assert (bool condition, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0) { - if (!condition) throw new Exception("C# assertion failed."); + if (!condition) throw new Exception($"C# assertion failed at {file}:{line}."); } } diff --git a/src/js/test/cs/Test.Library/Modules/Bidirectional.cs b/src/js/test/cs/Test.Library/Modules/Bidirectional.cs index 7d2153db..9e421821 100644 --- a/src/js/test/cs/Test.Library/Modules/Bidirectional.cs +++ b/src/js/test/cs/Test.Library/Modules/Bidirectional.cs @@ -4,9 +4,9 @@ namespace Test.Library; public class Bidirectional : IBidirectional { - public event Action? OnBiChanged; + public event Action? OnBiChanged; - public IBidirectional Bi { get; set => OnBiChanged?.Invoke(field = value); } = null!; + public IBidirectional? Bi { get; set => OnBiChanged?.Invoke(field = value); } = null!; - public IBidirectional EchoBi (IBidirectional bi) => bi; + public IBidirectional? EchoBi (IBidirectional? bi) => bi; } diff --git a/src/js/test/cs/Test.Library/Modules/ExportedModule.cs b/src/js/test/cs/Test.Library/Modules/ExportedModule.cs index 657117ed..f8e00d72 100644 --- a/src/js/test/cs/Test.Library/Modules/ExportedModule.cs +++ b/src/js/test/cs/Test.Library/Modules/ExportedModule.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Test.Library; @@ -8,9 +9,9 @@ public class ExportedModule : IExportedModule public Record? Record { get; set => OnRecordChanged?.Invoke(field = value); } - public async Task GetInstanceAsync (string arg) + public async Task GetInstanceAsync (string arg, Func? factory = null) { await Task.Delay(1); - return new ExportedInstanced(arg); + return factory?.Invoke() ?? new ExportedInstanced(arg); } } diff --git a/src/js/test/cs/Test.Library/Modules/IBidirectional.cs b/src/js/test/cs/Test.Library/Modules/IBidirectional.cs index 0c0277fa..50bc5ba0 100644 --- a/src/js/test/cs/Test.Library/Modules/IBidirectional.cs +++ b/src/js/test/cs/Test.Library/Modules/IBidirectional.cs @@ -4,9 +4,9 @@ namespace Test.Library; public interface IBidirectional { - event Action? OnBiChanged; + event Action? OnBiChanged; - IBidirectional Bi { get; set; } + IBidirectional? Bi { get; set; } - IBidirectional EchoBi (IBidirectional bi); + IBidirectional? EchoBi (IBidirectional? bi); } diff --git a/src/js/test/cs/Test.Library/Modules/IExportedModule.cs b/src/js/test/cs/Test.Library/Modules/IExportedModule.cs index 4c978df1..6f97ae25 100644 --- a/src/js/test/cs/Test.Library/Modules/IExportedModule.cs +++ b/src/js/test/cs/Test.Library/Modules/IExportedModule.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Test.Library; @@ -10,5 +11,5 @@ public interface IExportedModule Record? Record { get; set; } - Task GetInstanceAsync (string arg); + Task GetInstanceAsync (string arg, Func? factory = null); } diff --git a/src/js/test/cs/Test.Library/Modules/IImportedModule.cs b/src/js/test/cs/Test.Library/Modules/IImportedModule.cs index 981baa05..167cad33 100644 --- a/src/js/test/cs/Test.Library/Modules/IImportedModule.cs +++ b/src/js/test/cs/Test.Library/Modules/IImportedModule.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Test.Library; @@ -10,5 +11,5 @@ public interface IImportedModule Record? Record { get; set; } - Task GetInstanceAsync (string arg); + Task GetInstanceAsync (string arg, Func? factory = null); } diff --git a/src/js/test/cs/Test.Library/Modules/Modules.cs b/src/js/test/cs/Test.Library/Modules/Modules.cs index 5681723f..876af025 100644 --- a/src/js/test/cs/Test.Library/Modules/Modules.cs +++ b/src/js/test/cs/Test.Library/Modules/Modules.cs @@ -20,6 +20,7 @@ public static async Task CanInteropWithImportedModuleAsync () Assert(imported.Record == null); var instance = await imported.GetInstanceAsync("module-arg"); Assert(instance.GetInstanceArg() == "module-arg"); + Assert(await imported.GetInstanceAsync("", () => instance) == instance); Assert((await tcs.Task)?.Id == "event-rec"); imported.OnRecordChanged -= handler; } @@ -64,8 +65,9 @@ public static void CanInteropWithBidirectional () var js = ImportBi(); var cs = new Bidirectional(); IBidirectional? observed = null; - Action handler = b => observed = b; + Action handler = b => observed = b; js.OnBiChanged += handler; + Assert(js.EchoBi(null) == null); Assert(js.EchoBi(js) == js); Assert(js.EchoBi(cs) == cs); js.Bi = cs; @@ -74,6 +76,9 @@ public static void CanInteropWithBidirectional () js.Bi = js; Assert(observed == js); Assert(js.Bi == js); + js.Bi = null; + Assert(observed == null); + Assert(js.Bi == null); js.OnBiChanged -= handler; } diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index 49b3915f..46633c0d 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Bootsharp; +using Test.Library; namespace Test; @@ -43,8 +44,19 @@ public readonly record struct Union private Union (string shared) => Shared = shared; } -public readonly record struct ItemA (string? String, IReadOnlyDictionary? Map); -public readonly record struct ItemB (string[] Strings, IReadOnlyCollection Times, IReadOnlyList? Ints); +public readonly record struct ItemA ( + string? String, + IReadOnlyDictionary? Map, + IBidirectional? Bi, + RecordChanged? Changed +); + +public readonly record struct ItemB ( + string[] Strings, + IReadOnlyCollection Times, + IReadOnlyList? Ints, + Func?>? GetChanged +); public static class Serialization { diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 79ecd805..876deab2 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -25,12 +25,11 @@ class ImportedInner implements IImportedInnerInstanced { } class BidirectionalJS implements IBidirectional { - onBiChanged = new Event<[IBidirectional]>(); - #bi: IBidirectional; - constructor() { this.#bi = this; } + onBiChanged = new Event<[IBidirectional | undefined]>(); + #bi?: IBidirectional; get bi() { return this.#bi; } set bi(value) { this.onBiChanged.broadcast(this.#bi = value); } - echoBi(bi: IBidirectional) { return bi; } + echoBi(bi?: IBidirectional) { return bi ?? null; } } describe("while bootsharp is not booted", () => { @@ -92,9 +91,9 @@ describe("while bootsharp is booted", () => { it("can interop with imported modules", async () => { let record: Record | undefined = { id: "initial" }; IImportedModule.record = { get: () => record, set: v => record = v }; - IImportedModule.getInstanceAsync = async (arg) => { + IImportedModule.getInstanceAsync = async (arg, factory) => { await new Promise(res => setTimeout(res, 1)); - return new Imported(arg); + return factory?.() ?? new Imported(arg); }; const promise = Modules.canInteropWithImportedModuleAsync(); IImportedModule.onRecordChanged.broadcast({ id: "event-rec" }); @@ -111,6 +110,7 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(undefined); const inst = await IExportedModule.getInstanceAsync("module-arg"); expect(inst.getInstanceArg()).toStrictEqual("module-arg"); + expect(await IExportedModule.getInstanceAsync("", () => inst)).toStrictEqual(inst); IExportedModule.onRecordChanged.unsubscribe(handler); }); it("can interop with imported instances", async () => { @@ -146,6 +146,7 @@ describe("while bootsharp is booted", () => { const js = new BidirectionalJS(); const handler = vi.fn(); exp.onBiChanged.subscribe(handler); + expect(exp.echoBi(undefined)).toBe(null); expect(exp.echoBi(exp)).toBe(exp); expect(exp.echoBi(js)).toBe(js); exp.bi = js; @@ -154,6 +155,9 @@ describe("while bootsharp is booted", () => { exp.bi = exp; expect(handler).toHaveBeenCalledWith(exp); expect(exp.bi).toBe(exp); + exp.bi = undefined; + expect(handler).toHaveBeenCalledWith(undefined); + expect(exp.bi).toBe(undefined); exp.onBiChanged.unsubscribe(handler); Modules.canInteropWithBidirectional(); }); diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index efa076af..24e9ceb9 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, it } from "vitest"; import { bootRuntime } from "../cs"; -import { Serialization } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; +import { Serialization, ItemA } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; import type { Primitives, Union } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; -import { Registries, IRegistryProvider, TrackType } from "../cs/Test/bin/bootsharp/generated/modules/test/library.g.mjs"; +import { Registries, IRegistryProvider, Modules, TrackType, Record, IBidirectional } from "../cs/Test/bin/bootsharp/generated/modules/test/library.g.mjs"; describe("serialization", () => { beforeAll(bootRuntime); @@ -58,6 +58,18 @@ describe("serialization", () => { expect(Serialization.echoUnions([a, b, null])).toStrictEqual([a, b, null]); expect(Serialization.echoUnions(undefined)).toBeNull(); }); + it("instances survive serialization", () => { + const bi = Modules.exportBi(); + const changed = (_?: ItemA, __?: Record) => {}; + const getChanged = (b: IBidirectional) => b === bi ? changed : null; + const echoed = Serialization.echoUnions([{ + shared: "", a: { bi, changed }, b: { strings: [], times: [], getChanged } + }])![0]!; + expect(echoed.a!.bi).toStrictEqual(bi); + expect(echoed.a!.changed).toStrictEqual(changed); + expect(echoed.b!.getChanged).toStrictEqual(getChanged); + expect(echoed.b!.getChanged!(bi)).toStrictEqual(changed); + }); it("can echo unions with all nullable fields omitted", () => { const a: Union = { shared: "A", a: {} }; const b: Union = { shared: "B", b: { strings: ["x"], times: [new Date()] } }; From 4e1819571656665ea0b22c8cc374b4eceaf72425 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 23 May 2026 19:38:20 +0300 Subject: [PATCH 7/9] add test --- src/js/test/spec/serialization.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 24e9ceb9..5813e2ed 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; import { bootRuntime } from "../cs"; import { Serialization, ItemA } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; import type { Primitives, Union } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; @@ -60,7 +60,8 @@ describe("serialization", () => { }); it("instances survive serialization", () => { const bi = Modules.exportBi(); - const changed = (_?: ItemA, __?: Record) => {}; + const handler = vi.fn(); + const changed = (item?: ItemA, record?: Record) => handler(item, record); const getChanged = (b: IBidirectional) => b === bi ? changed : null; const echoed = Serialization.echoUnions([{ shared: "", a: { bi, changed }, b: { strings: [], times: [], getChanged } @@ -69,6 +70,8 @@ describe("serialization", () => { expect(echoed.a!.changed).toStrictEqual(changed); expect(echoed.b!.getChanged).toStrictEqual(getChanged); expect(echoed.b!.getChanged!(bi)).toStrictEqual(changed); + echoed.b!.getChanged!(bi)!(bi, undefined); + expect(handler).toHaveBeenCalledWith(bi, undefined); }); it("can echo unions with all nullable fields omitted", () => { const a: Union = { shared: "A", a: {} }; From 28681915598c70c64f13cada15b67340350b7eeb Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 23 May 2026 20:42:33 +0300 Subject: [PATCH 8/9] cover --- src/js/test/cs/Test/Serialization.cs | 11 +++++++ src/js/test/spec/serialization.spec.ts | 45 +++++++++++++++++--------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index 46633c0d..cc03d697 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -79,4 +79,15 @@ public static class Serialization [Export] public static IReadOnlyCollection EchoReadOnlyCollection (IReadOnlyCollection value) => value; [Export] public static IDictionary EchoDictionaryInterface (IDictionary value) => value; [Export] public static IReadOnlyDictionary EchoReadOnlyDictionary (IReadOnlyDictionary value) => value; + + [Export] + public static void ImportedInstancesSurviveSerialization (Union union, IBidirectional bi) + { + Assert(union.A?.Bi == bi); + var biChanged = union.B?.GetChanged?.Invoke(bi); + Assert(biChanged != null); + Assert(union.B?.GetChanged?.Invoke(new Bidirectional()) == null); + union.A?.Changed?.Invoke(union.A, new Record("a-rec")); + biChanged!.Invoke(bi, new Record("bi-rec")); + } } diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 5813e2ed..20b31620 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -1,7 +1,7 @@ import { beforeAll, describe, expect, it, vi } from "vitest"; import { bootRuntime } from "../cs"; -import { Serialization, ItemA } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; import type { Primitives, Union } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; +import { Serialization, ItemA } from "../cs/Test/bin/bootsharp/generated/modules/test.g.mjs"; import { Registries, IRegistryProvider, Modules, TrackType, Record, IBidirectional } from "../cs/Test/bin/bootsharp/generated/modules/test/library.g.mjs"; describe("serialization", () => { @@ -58,21 +58,6 @@ describe("serialization", () => { expect(Serialization.echoUnions([a, b, null])).toStrictEqual([a, b, null]); expect(Serialization.echoUnions(undefined)).toBeNull(); }); - it("instances survive serialization", () => { - const bi = Modules.exportBi(); - const handler = vi.fn(); - const changed = (item?: ItemA, record?: Record) => handler(item, record); - const getChanged = (b: IBidirectional) => b === bi ? changed : null; - const echoed = Serialization.echoUnions([{ - shared: "", a: { bi, changed }, b: { strings: [], times: [], getChanged } - }])![0]!; - expect(echoed.a!.bi).toStrictEqual(bi); - expect(echoed.a!.changed).toStrictEqual(changed); - expect(echoed.b!.getChanged).toStrictEqual(getChanged); - expect(echoed.b!.getChanged!(bi)).toStrictEqual(changed); - echoed.b!.getChanged!(bi)!(bi, undefined); - expect(handler).toHaveBeenCalledWith(bi, undefined); - }); it("can echo unions with all nullable fields omitted", () => { const a: Union = { shared: "A", a: {} }; const b: Union = { shared: "B", b: { strings: ["x"], times: [new Date()] } }; @@ -131,4 +116,32 @@ describe("serialization", () => { expect(Serialization.echoDictionary(undefined)).toBeNull(); expect(Serialization.echoNestedDictionary(undefined)).toBeNull(); }); + it("imported instances survive serialization", () => { + const bi = Modules.exportBi(); + const aHandler = vi.fn(); + const biHandler = vi.fn(); + const changed = (item?: ItemA, record?: Record) => aHandler(item, record); + const biChanged = (b?: IBidirectional, record?: Record) => biHandler(b, record); + const getChanged = (b: IBidirectional) => b === bi ? biChanged : null; + Serialization.importedInstancesSurviveSerialization( + { shared: "", a: { bi, changed }, b: { strings: [], times: [], getChanged } }, bi + ); + expect(aHandler).toHaveBeenCalledWith(expect.objectContaining({ bi, changed }), { id: "a-rec" }); + expect(biHandler).toHaveBeenCalledWith(bi, { id: "bi-rec" }); + }); + it("exported instances survive serialization", () => { + const bi = Modules.exportBi(); + const handler = vi.fn(); + const changed = (item?: ItemA, record?: Record) => handler(item, record); + const getChanged = (b: IBidirectional) => b === bi ? changed : null; + const echoed = Serialization.echoUnions([{ + shared: "", a: { bi, changed }, b: { strings: [], times: [], getChanged } + }])![0]!; + expect(echoed.a!.bi).toStrictEqual(bi); + expect(echoed.a!.changed).toStrictEqual(changed); + expect(echoed.b!.getChanged).toStrictEqual(getChanged); + expect(echoed.b!.getChanged!(bi)).toStrictEqual(changed); + echoed.b!.getChanged!(bi)!(bi, undefined); + expect(handler).toHaveBeenCalledWith(bi, undefined); + }); }); From f95c539d506669a8fb53e19bf781b230c7f5a914 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 23 May 2026 21:03:12 +0300 Subject: [PATCH 9/9] cover --- .../Modules/{Bidirectional.cs => BidirectionalCS.cs} | 2 +- .../test/cs/Test.Library/Modules/ExportedInstanced.cs | 7 +++++++ src/js/test/cs/Test.Library/Modules/ExportedModule.cs | 5 ++--- .../cs/Test.Library/Modules/IExportedInstanced.cs | 2 ++ .../test/cs/Test.Library/Modules/IExportedModule.cs | 3 +-- .../cs/Test.Library/Modules/IImportedInstanced.cs | 2 ++ .../test/cs/Test.Library/Modules/IImportedModule.cs | 3 +-- src/js/test/cs/Test.Library/Modules/Modules.cs | 7 ++++--- src/js/test/cs/Test/Serialization.cs | 2 +- src/js/test/spec/interop.spec.ts | 11 ++++++++--- 10 files changed, 29 insertions(+), 15 deletions(-) rename src/js/test/cs/Test.Library/Modules/{Bidirectional.cs => BidirectionalCS.cs} (84%) diff --git a/src/js/test/cs/Test.Library/Modules/Bidirectional.cs b/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs similarity index 84% rename from src/js/test/cs/Test.Library/Modules/Bidirectional.cs rename to src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs index 9e421821..3fe25981 100644 --- a/src/js/test/cs/Test.Library/Modules/Bidirectional.cs +++ b/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs @@ -2,7 +2,7 @@ namespace Test.Library; -public class Bidirectional : IBidirectional +public class BidirectionalCS : IBidirectional { public event Action? OnBiChanged; diff --git a/src/js/test/cs/Test.Library/Modules/ExportedInstanced.cs b/src/js/test/cs/Test.Library/Modules/ExportedInstanced.cs index 1cb91efb..b8fe88db 100644 --- a/src/js/test/cs/Test.Library/Modules/ExportedInstanced.cs +++ b/src/js/test/cs/Test.Library/Modules/ExportedInstanced.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Test.Library; @@ -16,4 +17,10 @@ public async Task GetRecordIdAsync (Record record) await Task.Delay(1); return record.Id; } + + public async Task GetBiAsync (Func? factory = null) + { + await Task.Delay(1); + return factory?.Invoke() ?? new BidirectionalCS(); + } } diff --git a/src/js/test/cs/Test.Library/Modules/ExportedModule.cs b/src/js/test/cs/Test.Library/Modules/ExportedModule.cs index f8e00d72..657117ed 100644 --- a/src/js/test/cs/Test.Library/Modules/ExportedModule.cs +++ b/src/js/test/cs/Test.Library/Modules/ExportedModule.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; namespace Test.Library; @@ -9,9 +8,9 @@ public class ExportedModule : IExportedModule public Record? Record { get; set => OnRecordChanged?.Invoke(field = value); } - public async Task GetInstanceAsync (string arg, Func? factory = null) + public async Task GetInstanceAsync (string arg) { await Task.Delay(1); - return factory?.Invoke() ?? new ExportedInstanced(arg); + return new ExportedInstanced(arg); } } diff --git a/src/js/test/cs/Test.Library/Modules/IExportedInstanced.cs b/src/js/test/cs/Test.Library/Modules/IExportedInstanced.cs index a2836164..3887eace 100644 --- a/src/js/test/cs/Test.Library/Modules/IExportedInstanced.cs +++ b/src/js/test/cs/Test.Library/Modules/IExportedInstanced.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Test.Library; @@ -11,4 +12,5 @@ public interface IExportedInstanced string GetInstanceArg (); Task GetRecordIdAsync (Record record); + Task GetBiAsync (Func? factory = null); } diff --git a/src/js/test/cs/Test.Library/Modules/IExportedModule.cs b/src/js/test/cs/Test.Library/Modules/IExportedModule.cs index 6f97ae25..4c978df1 100644 --- a/src/js/test/cs/Test.Library/Modules/IExportedModule.cs +++ b/src/js/test/cs/Test.Library/Modules/IExportedModule.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; namespace Test.Library; @@ -11,5 +10,5 @@ public interface IExportedModule Record? Record { get; set; } - Task GetInstanceAsync (string arg, Func? factory = null); + Task GetInstanceAsync (string arg); } diff --git a/src/js/test/cs/Test.Library/Modules/IImportedInstanced.cs b/src/js/test/cs/Test.Library/Modules/IImportedInstanced.cs index a19c16b1..7531edd5 100644 --- a/src/js/test/cs/Test.Library/Modules/IImportedInstanced.cs +++ b/src/js/test/cs/Test.Library/Modules/IImportedInstanced.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Test.Library; @@ -11,4 +12,5 @@ public interface IImportedInstanced string GetInstanceArg (); Task GetRecordIdAsync (Record record); + Task GetBiAsync (Func? factory = null); } diff --git a/src/js/test/cs/Test.Library/Modules/IImportedModule.cs b/src/js/test/cs/Test.Library/Modules/IImportedModule.cs index 167cad33..981baa05 100644 --- a/src/js/test/cs/Test.Library/Modules/IImportedModule.cs +++ b/src/js/test/cs/Test.Library/Modules/IImportedModule.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; namespace Test.Library; @@ -11,5 +10,5 @@ public interface IImportedModule Record? Record { get; set; } - Task GetInstanceAsync (string arg, Func? factory = null); + Task GetInstanceAsync (string arg); } diff --git a/src/js/test/cs/Test.Library/Modules/Modules.cs b/src/js/test/cs/Test.Library/Modules/Modules.cs index 876af025..ac62c9c7 100644 --- a/src/js/test/cs/Test.Library/Modules/Modules.cs +++ b/src/js/test/cs/Test.Library/Modules/Modules.cs @@ -20,7 +20,6 @@ public static async Task CanInteropWithImportedModuleAsync () Assert(imported.Record == null); var instance = await imported.GetInstanceAsync("module-arg"); Assert(instance.GetInstanceArg() == "module-arg"); - Assert(await imported.GetInstanceAsync("", () => instance) == instance); Assert((await tcs.Task)?.Id == "event-rec"); imported.OnRecordChanged -= handler; } @@ -33,6 +32,8 @@ public static async Task CanInteropWithImportedInstanceAsync (IImportedInstanced imported.OnRecordChanged += handler; Assert(imported.GetInstanceArg() == "instance-arg"); Assert(await imported.GetRecordIdAsync(new Record("rec-id")) == "rec-id"); + Assert(await imported.GetBiAsync() is not BidirectionalCS); + Assert(await imported.GetBiAsync(() => new BidirectionalCS()) is BidirectionalCS); Assert(imported.Record?.Id == "initial-rec"); imported.Record = new Record("set"); Assert(imported.Record?.Id == "set"); @@ -56,14 +57,14 @@ public static void CanInteropWithImportedInnerInstance (IImportedInstanced impor inner.OnCountChanged -= handler; } - [Export] public static IBidirectional ExportBi () => new Bidirectional(); + [Export] public static IBidirectional ExportBi () => new BidirectionalCS(); [Import] public static partial IBidirectional ImportBi (); [Export] public static void CanInteropWithBidirectional () { var js = ImportBi(); - var cs = new Bidirectional(); + var cs = new BidirectionalCS(); IBidirectional? observed = null; Action handler = b => observed = b; js.OnBiChanged += handler; diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index cc03d697..2a20659e 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -86,7 +86,7 @@ public static void ImportedInstancesSurviveSerialization (Union union, IBidirect Assert(union.A?.Bi == bi); var biChanged = union.B?.GetChanged?.Invoke(bi); Assert(biChanged != null); - Assert(union.B?.GetChanged?.Invoke(new Bidirectional()) == null); + Assert(union.B?.GetChanged?.Invoke(new BidirectionalCS()) == null); union.A?.Changed?.Invoke(union.A, new Record("a-rec")); biChanged!.Invoke(bi, new Record("bi-rec")); } diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 876deab2..cfc6a268 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -14,6 +14,10 @@ class Imported implements IImportedInstanced { await new Promise(res => setTimeout(res, 1)); return record.id; } + async getBiAsync(factory?: () => IBidirectional): Promise { + await new Promise(res => setTimeout(res, 1)); + return factory?.() ?? new BidirectionalJS(); + } } class ImportedInner implements IImportedInnerInstanced { @@ -91,9 +95,9 @@ describe("while bootsharp is booted", () => { it("can interop with imported modules", async () => { let record: Record | undefined = { id: "initial" }; IImportedModule.record = { get: () => record, set: v => record = v }; - IImportedModule.getInstanceAsync = async (arg, factory) => { + IImportedModule.getInstanceAsync = async (arg) => { await new Promise(res => setTimeout(res, 1)); - return factory?.() ?? new Imported(arg); + return new Imported(arg); }; const promise = Modules.canInteropWithImportedModuleAsync(); IImportedModule.onRecordChanged.broadcast({ id: "event-rec" }); @@ -110,7 +114,6 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(undefined); const inst = await IExportedModule.getInstanceAsync("module-arg"); expect(inst.getInstanceArg()).toStrictEqual("module-arg"); - expect(await IExportedModule.getInstanceAsync("", () => inst)).toStrictEqual(inst); IExportedModule.onRecordChanged.unsubscribe(handler); }); it("can interop with imported instances", async () => { @@ -125,6 +128,8 @@ describe("while bootsharp is booted", () => { const handler = vi.fn(); expect(exported.getInstanceArg()).toStrictEqual("instance-arg"); expect(await exported.getRecordIdAsync({ id: "rec" })).toStrictEqual("rec"); + expect(await exported.getBiAsync()).not.toBeInstanceOf(BidirectionalJS); + expect(await exported.getBiAsync(() => new BidirectionalJS())).toBeInstanceOf(BidirectionalJS); expect(exported.record).toBeUndefined(); exported.onRecordChanged.subscribe(handler); exported.record = { id: "set" };