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/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.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/CSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs new file mode 100644 index 00000000..34b0e617 --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs @@ -0,0 +1,241 @@ +namespace Bootsharp.Publish.Test; + +public class CSInstanceTest : GenerateCSTest +{ + protected override string TestedContent => GeneratedInstances; + + [Fact] + public void GeneratesImportedInstanceInterface () + { + AddAssembly(With( + """ + public record Record; + + public interface IImported + { + delegate void SomethingChanged(); + + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; + + Record? Record { get; set; } + + void Fun (string arg); + } + + public class Class + { + [Import] public static IImported GetImported () => Proxies.Get>("Class.GetImported")(); + } + """)); + Execute(); + Contains( + """ + public class JS_Import_IImported (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 + { + 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); + } + """); + } + + [Fact] + public void DoesNotGenerateExportedInstanceInterface () + { + AddAssembly(With( + """ + public record Record; + + public interface IExported + { + delegate void SomethingChanged(); + + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; + + Record? Record { get; set; } + + void Fun (string arg); + } + + public class Class + { + [Export] public static IExported GetExported () => default; + } + """)); + Execute(); + DoesNotContain("JSExported"); + } + + [Fact] + public void IgnoresImplementedInterfaceMethods () + { + AddAssembly(With( + """ + public interface IExported { int Foo () => 0; } + public interface IImported { int Foo () => 0; } + + public class Class + { + [Export] public static IExported GetExported () => default; + [Import] public static IExported GetImported () => default; + } + """)); + Execute(); + DoesNotContain("Foo"); + } + + [Fact] + public void GeneratesSpecializedExportsForInstancesWithEvents () + { + AddAssembly(With( + """ + public record Record; + + public interface IExported { event Action Changed; } + public interface IImported { event Action Changed; } + + public partial class Class + { + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; + } + """)); + 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)); + }); + """); + } + + [Fact] + public void DoesNotGenerateDuplicateSpecializedExports () + { + AddAssembly(With( + """ + public interface IBi + { + event Action? Changed; + event Action? Done; + } + + public class Class + { + [Export] public static IBi GetExported () => default!; + [Import] public static IBi GetImported () => default!; + } + """)); + Execute(); + Once(@"internal static int Export \(global::IBi it\)"); + } + + [Fact] + public void GeneratesImportProxyForBidirectionalProperty () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IModule))] + + public interface IInstanced; + 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 81% rename from src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs index 1b4daf7e..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; @@ -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] @@ -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 () { @@ -465,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/CSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSModuleTest.cs new file mode 100644 index 00000000..4a030e46 --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSModuleTest.cs @@ -0,0 +1,320 @@ +namespace Bootsharp.Publish.Test; + +public class CSModuleTest : GenerateCSTest +{ + protected override string TestedContent => GeneratedModules; + + [Fact] + public void GeneratesExportedInterfaceModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IExported))] + + public record Record; + + public interface IExported + { + delegate void SomethingChanged(); + + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; + + Record? Record { get; set; } + + void Inv (string? a); + Task InvAsync (); + Record? InvRecord (); + Task InvAsyncResult (); + string[] InvArray (int[] a); + } + """)); + Execute(); + Contains( + """ + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () + { + 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))); + } + } + + public class JS_Export_IExported + { + private static global::IExported handler = null!; + + public JS_Export_IExported (global::IExported handler) + { + 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); + } + """); + } + + [Fact] + public void GeneratesExportedClassModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Exported))] + + public record Record; + + public class Exported + { + public delegate void SomethingChanged(); + + public event Action OnRecordChanged; + public event SomethingChanged OnSomethingChanged; + + public Record? Record { get; set; } + + public virtual void Inv (string? a) {} + public Task InvAsync () => Task.CompletedTask; + public Record? InvRecord () => null; + public Task InvAsyncResult () => Task.FromResult(""); + public string[] InvArray (int[] a) => []; + } + """)); + Execute(); + Contains( + """ + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () + { + 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))); + } + } + + public class JS_Export_Exported + { + private static global::Exported handler = null!; + + public JS_Export_Exported (global::Exported handler) + { + 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); + } + """); + } + + [Fact] + public void DoesNotGenerateExportedStaticClassModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(StaticExported))] + + public static class StaticExported + { + public static void Inv () {} + } + """)); + Execute(); + DoesNotContain("JSStaticExported"); + } + + [Fact] + public void GeneratesImportedInterfaceModule () + { + AddAssembly(With( + """ + [assembly:Import(typeof(IImported))] + + public record Record; + + public interface IImported + { + delegate void SomethingChanged(); + + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; + + Record? Record { get; set; } + + void Inv (string? a); + Task InvAsync (); + Record? InvRecord (); + Task InvAsyncResult (); + string[] InvArray (int[] a); + } + """)); + Execute(); + Contains( + """ + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () + { + Modules.Register(typeof(global::IImported), new ImportModule(new global::Bootsharp.Generated.JS_Import_IImported())); + } + } + + public class JS_Import_IImported : 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 + { + 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); + } + """); + } + + [Fact] + public void DoesNotGenerateImportedClassModule () + { + AddAssembly(With( + """ + [assembly:Import(typeof(Imported))] + + public class Imported + { + public void Inv () {} + } + """)); + Execute(); + DoesNotContain("JSImported"); + } + + [Fact] + public void RespectsModuleNamespace () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public record Record; + + public interface IExported { void Inv (Record a); } + public interface IImported { void Fun (Record a); } + """)); + Execute(); + Contains( + """ + namespace Bootsharp.Generated; + + internal static class ModuleRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () + { + 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())); + } + } + + public class JS_Export_Space_IExported + { + private static global::Space.IExported handler = null!; + + 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); + } + + public class JS_Import_Space_IImported : global::Space.IImported + { + void global::Space.IImported.Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.JS_Import_Space_IImported_Fun(a); + } + """); + } + + [Fact] + public void IgnoresImplementedInterfaceMethods () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IExported))] + [assembly:Import(typeof(IImported))] + + public interface IExported { int Foo () => 0; } + public interface IImported { int Foo () => 0; } + """)); + Execute(); + DoesNotContain("Foo"); + } + + [Fact] + public void IgnoresStaticMembersOnExportedClassModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Exported))] + + public class Exported + { + public static void StaticMethod () {} + public void Inst () {} + } + """)); + Execute(); + DoesNotContain("StaticMethod"); + } + + [Fact] + public void IgnoresDuplicateModules () + { + AddAssembly("Library.dll", With( + """ + [assembly:Import(typeof(IShared))] + public interface IShared { void Inv (); } + """)); + AddAssembly("Entry.dll", With( + """ + [assembly:Import(typeof(IShared))] + """)); + Execute(); + Once("class JS_Import_IShared"); + } +} 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/GenerateCS/InstancesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs deleted file mode 100644 index ae869405..00000000 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace Bootsharp.Publish.Test; - -public class InstancesTest : GenerateCSTest -{ - protected override string TestedContent => GeneratedInstances; - - [Fact] - public void GeneratesImportedInstanceInterface () - { - AddAssembly(With( - """ - public record Record; - - public interface IImported - { - delegate void SomethingChanged(); - - event Action OnRecordChanged; - event SomethingChanged OnSomethingChanged; - - Record? Record { get; set; } - - void Fun (string arg); - } - - public class Class - { - [Import] public static IImported GetImported () => Proxies.Get>("Class.GetImported")(); - } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated.Imports - { - public class JSImported (int id) : global::Bootsharp.JSProxy(id), global::IImported - { - ~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); - } - } - """); - } - - [Fact] - public void DoesNotGenerateExportedInstanceInterface () - { - AddAssembly(With( - """ - public record Record; - - public interface IExported - { - delegate void SomethingChanged(); - - event Action OnRecordChanged; - event SomethingChanged OnSomethingChanged; - - Record? Record { get; set; } - - void Fun (string arg); - } - - public class Class - { - [Export] public static IExported GetExported () => default; - } - """)); - Execute(); - DoesNotContain("JSExported"); - } - - [Fact] - public void IgnoresImplementedInterfaceMethods () - { - AddAssembly(With( - """ - public interface IExported { int Foo () => 0; } - public interface IImported { int Foo () => 0; } - - public class Class - { - [Export] public static IExported GetExported () => default; - [Import] public static IExported GetImported () => default; - } - """)); - Execute(); - DoesNotContain("Foo"); - } - - [Fact] - public void GeneratesSpecializedExportsForInstancesWithEvents () - { - AddAssembly(With( - """ - public record Record; - - public interface IExported { event Action Changed; } - public interface IImported { event Action Changed; } - - public partial class Class - { - [Export] public static IExported GetExported (IImported it) => default; - [Import] public static IImported GetImported (IExported it) => default; - } - """)); - 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)); - }); - """); - } - - [Fact] - public void DoesNotGenerateDuplicateSpecializedExports () - { - AddAssembly(With( - """ - public interface IBi - { - event Action? Changed; - event Action? Done; - } - - public class Class - { - [Export] public static IBi GetExported () => default!; - [Import] public static IBi GetImported () => default!; - } - """)); - Execute(); - Once(@"internal static int Export \(global::IBi it\)"); - } - - [Fact] - public void GeneratesImportProxyForBidirectionalProperty () - { - AddAssembly(With( - """ - [assembly:Export(typeof(IModule))] - - public interface IInstanced; - public interface IModule { IInstanced Item { get; set; } } - """)); - Execute(); - Contains("public class JSInstanced (int id) : global::Bootsharp.JSProxy(id), global::IInstanced"); - } -} diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs deleted file mode 100644 index e055af97..00000000 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs +++ /dev/null @@ -1,339 +0,0 @@ -namespace Bootsharp.Publish.Test; - -public class ModulesTest : GenerateCSTest -{ - protected override string TestedContent => GeneratedModules; - - [Fact] - public void GeneratesExportedInterfaceModule () - { - AddAssembly(With( - """ - [assembly:Export(typeof(IExported))] - - public record Record; - - public interface IExported - { - delegate void SomethingChanged(); - - event Action OnRecordChanged; - event SomethingChanged OnSomethingChanged; - - Record? Record { get; set; } - - void Inv (string? a); - Task InvAsync (); - Record? InvRecord (); - Task InvAsyncResult (); - string[] InvArray (int[] a); - } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class ModuleRegistrations - { - [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))); - } - } - } - - namespace Bootsharp.Generated.Exports - { - public class JSExported - { - 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); - } - } - """); - } - - [Fact] - public void GeneratesExportedClassModule () - { - AddAssembly(With( - """ - [assembly:Export(typeof(Exported))] - - public record Record; - - public class Exported - { - public delegate void SomethingChanged(); - - public event Action OnRecordChanged; - public event SomethingChanged OnSomethingChanged; - - public Record? Record { get; set; } - - public virtual void Inv (string? a) {} - public Task InvAsync () => Task.CompletedTask; - public Record? InvRecord () => null; - public Task InvAsyncResult () => Task.FromResult(""); - public string[] InvArray (int[] a) => []; - } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class ModuleRegistrations - { - [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))); - } - } - } - - namespace Bootsharp.Generated.Exports - { - public class JSExported - { - 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); - } - } - """); - } - - [Fact] - public void DoesNotGenerateExportedStaticClassModule () - { - AddAssembly(With( - """ - [assembly:Export(typeof(StaticExported))] - - public static class StaticExported - { - public static void Inv () {} - } - """)); - Execute(); - DoesNotContain("JSStaticExported"); - } - - [Fact] - public void GeneratesImportedInterfaceModule () - { - AddAssembly(With( - """ - [assembly:Import(typeof(IImported))] - - public record Record; - - public interface IImported - { - delegate void SomethingChanged(); - - event Action OnRecordChanged; - event SomethingChanged OnSomethingChanged; - - Record? Record { get; set; } - - void Inv (string? a); - Task InvAsync (); - Record? InvRecord (); - Task InvAsyncResult (); - string[] InvArray (int[] a); - } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class ModuleRegistrations - { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterModules () - { - Modules.Register(typeof(global::IImported), new ImportModule(new global::Bootsharp.Generated.Imports.JSImported())); - } - } - } - - namespace Bootsharp.Generated.Imports - { - 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 - { - 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); - } - } - """); - } - - [Fact] - public void DoesNotGenerateImportedClassModule () - { - AddAssembly(With( - """ - [assembly:Import(typeof(Imported))] - - public class Imported - { - public void Inv () {} - } - """)); - Execute(); - DoesNotContain("JSImported"); - } - - [Fact] - public void RespectsModuleNamespace () - { - AddAssembly(With( - """ - [assembly:Export(typeof(Space.IExported))] - [assembly:Import(typeof(Space.IImported))] - - namespace Space; - - public record Record; - - public interface IExported { void Inv (Record a); } - public interface IImported { void Fun (Record a); } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class ModuleRegistrations - { - [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())); - } - } - } - - namespace Bootsharp.Generated.Exports.Space - { - public class JSExported - { - 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); - } - } - - namespace Bootsharp.Generated.Imports.Space - { - 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); - } - } - """); - } - - [Fact] - public void IgnoresImplementedInterfaceMethods () - { - AddAssembly(With( - """ - [assembly:Export(typeof(IExported))] - [assembly:Import(typeof(IImported))] - - public interface IExported { int Foo () => 0; } - public interface IImported { int Foo () => 0; } - """)); - Execute(); - DoesNotContain("Foo"); - } - - [Fact] - public void IgnoresStaticMembersOnExportedClassModule () - { - AddAssembly(With( - """ - [assembly:Export(typeof(Exported))] - - public class Exported - { - public static void StaticMethod () {} - public void Inst () {} - } - """)); - Execute(); - DoesNotContain("StaticMethod"); - } - - [Fact] - public void IgnoresDuplicateModules () - { - AddAssembly("Library.dll", With( - """ - [assembly:Import(typeof(IShared))] - public interface IShared { void Inv (); } - """)); - AddAssembly("Entry.dll", With( - """ - [assembly:Import(typeof(IShared))] - """)); - Execute(); - Once("class JSShared"); - } -} diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index 08675398..bb803de1 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(); @@ -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]>; } """); @@ -354,13 +354,13 @@ 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; } }"), - 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; @@ -373,15 +373,15 @@ export interface Foo { } [Fact] - public void DefinitionIsGeneratedForInterfaceAndImplementation () + 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; @@ -400,14 +400,14 @@ export interface Interface { } [Fact] - public void DefinitionIsGeneratedForTypeWithListProperty () + 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; @@ -421,14 +421,14 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithJaggedArrayProperty () + 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; @@ -442,14 +442,14 @@ export interface Item { } [Fact] - public void DefinitionIsGeneratedForTypeWithReadOnlyListProperty () + 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; @@ -463,14 +463,14 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithDictionaryProperty () + 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; @@ -484,14 +484,14 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithReadOnlyDictionaryProperty () + 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; @@ -505,14 +505,14 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithCollectionProperty () + 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; @@ -526,14 +526,14 @@ export interface Container { } [Fact] - public void DefinitionIsGeneratedForTypeWithReadOnlyCollectionProperty () + 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; @@ -547,14 +547,14 @@ 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; } }"), - 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; @@ -570,14 +570,14 @@ export interface GenericNull { } [Fact] - public void DefinitionIsGeneratedForGenericRecord () + 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; @@ -592,13 +592,13 @@ export namespace Class { } [Fact] - public void DefinitionIsGeneratedForGenericInterface () + 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; @@ -610,14 +610,14 @@ export interface IGenericInterface { } [Fact] - public void DefinitionIsGeneratedForNestedGenericTypes () + 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; @@ -626,24 +626,84 @@ 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; } }"), - 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 () + { + 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 () { @@ -1045,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 () { @@ -1104,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; @@ -1126,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; @@ -1231,7 +1302,7 @@ public class Class } [Fact] - public void DeclarationsCrossNamespaceImportsEmitted () + public void CrossNamespaceImportsEmitted () { AddAssembly(With( """ @@ -1248,7 +1319,7 @@ public class Class { } [Fact] - public void DeclarationFileImportsRootNamespaceTypeFromPackageRoot () + public void GlobalNamespaceImportsFromIndex () { AddAssembly(With( """ @@ -1264,7 +1335,7 @@ public class Class { [Export] public static RootRecord Get () => default!; } } [Fact] - public void TypeDeclarationGroupsMultipleNestedTypes () + public void GroupsMultipleNestedTypes () { AddAssembly(With( """ @@ -1480,13 +1551,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/JSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs index acdd6e20..7b5e7147 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)); } }); }; """); @@ -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 () { @@ -220,7 +275,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 +304,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 fb65c9dd..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")"""); } @@ -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) }; """); } @@ -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/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..fda0458c 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 @@ -83,62 +91,60 @@ 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; } - 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"; 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 nil = (forceNil || IsNullable(nul)) ? "?" : ""; + 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/SurfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs index 8bf23dfe..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 . /// @@ -69,19 +83,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/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/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/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index 038632f4..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), @@ -93,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; @@ -127,7 +137,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 +147,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 +157,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 +166,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) }; @@ -176,13 +186,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/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs similarity index 56% rename from src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs index aa17d30f..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!; @@ -15,31 +16,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")}} @@ -47,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});"; } @@ -72,19 +74,38 @@ 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) => $$""" - namespace {{(this.it = it).Proxy.Space}} + public class {{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))}} } """; + 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 77% rename from src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs index 3f3836e2..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!; @@ -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/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 9dc28130..b56ac54e 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -2,8 +2,12 @@ namespace Bootsharp.Publish; +/// +/// Generates TypeScript type declarations. +/// internal sealed class DeclarationGenerator { + private readonly HashSet declared = []; private readonly CodeBuilder bld = new(); private readonly TypeSyntaxBuilder ts; private readonly DocumentationBuilder doc; @@ -21,6 +25,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 +47,15 @@ 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 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) DeclareNode(child); if (wrap) bld.Exit("}"); @@ -80,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); @@ -100,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}]>;"); } @@ -114,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)};"); } } @@ -129,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}]>;"); } @@ -149,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 8520476d..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,23 +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 args = string.Join(", ", type.GetGenericArguments().Select(Build)); - return $"{@ref}<{args}>"; + 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 disc = argTypes.Length > 1 ? argTypes.Length.ToString() : ""; + return $"{name}{disc}<{string.Join(", ", args)}>"; } private string BuildPrimitive (Type type) @@ -149,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 00aa7f15..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,9 +53,30 @@ 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.JS}} { + $i.{{it.Id}} = class {{it.Proxy.Id}} { {{Fmt([ "constructor(_id) { this._id = _id; }", ..it.Members.Select(EmitMember) @@ -76,11 +101,11 @@ private string EmitEvent (EventMeta evt) private string EmitMethod (MethodMeta method) { - var sigArgs = string.Join(", ", method.Args.Select(a => a.Name)); - 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 6d6ef416..412582df 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.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 deleted file mode 100644 index 7d2153db..00000000 --- a/src/js/test/cs/Test.Library/Modules/Bidirectional.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Test.Library; - -public class Bidirectional : IBidirectional -{ - public event Action? OnBiChanged; - - public IBidirectional Bi { get; set => OnBiChanged?.Invoke(field = value); } = null!; - - public IBidirectional EchoBi (IBidirectional bi) => bi; -} diff --git a/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs b/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs new file mode 100644 index 00000000..3fe25981 --- /dev/null +++ b/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs @@ -0,0 +1,12 @@ +using System; + +namespace Test.Library; + +public class BidirectionalCS : IBidirectional +{ + public event Action? OnBiChanged; + + public IBidirectional? Bi { get; set => OnBiChanged?.Invoke(field = value); } = null!; + + public IBidirectional? EchoBi (IBidirectional? bi) => bi; +} 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/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/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/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/Modules.cs b/src/js/test/cs/Test.Library/Modules/Modules.cs index 5681723f..ac62c9c7 100644 --- a/src/js/test/cs/Test.Library/Modules/Modules.cs +++ b/src/js/test/cs/Test.Library/Modules/Modules.cs @@ -32,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"); @@ -55,17 +57,18 @@ 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; + 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 +77,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..2a20659e 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 { @@ -67,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 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 79ecd805..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 { @@ -25,12 +29,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", () => { @@ -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" }; @@ -146,6 +151,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 +160,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..20b31620 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 { beforeAll, describe, expect, it, vi } from "vitest"; import { bootRuntime } from "../cs"; -import { Serialization } 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 { 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", () => { beforeAll(bootRuntime); @@ -116,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); + }); });