From 88ef686a3b9913e9b9891a2985b6187f71699161 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 11 May 2026 13:14:16 +0300 Subject: [PATCH 01/13] implement --- .gitignore | 2 +- AGENTS.md | 22 +- CLAUDE.md | 1 + docs/guide/declarations.md | 142 +-- docs/guide/namespaces.md | 47 +- docs/guide/preferences.md | 6 +- samples/react/src/computer.tsx | 4 +- src/cs/Bootsharp.Common.Test/InstancesTest.cs | 46 +- .../Attributes/PreferencesAttribute.cs | 37 +- src/cs/Bootsharp.Common/Interop/Instances.cs | 59 +- src/cs/Bootsharp.Common/Interop/JSProxy.cs | 12 + src/cs/Bootsharp.Common/Interop/Modules.cs | 6 +- src/cs/Bootsharp.Common/Interop/Proxies.cs | 2 +- .../ImportPropertyTest.cs | 20 +- src/cs/Bootsharp.Generate/ImportProperty.cs | 8 +- .../GenerateCSTest.cs} | 8 +- .../{Emit => GenerateCS}/InstancesTest.cs | 39 +- .../{Emit => GenerateCS}/InteropTest.cs | 158 +-- .../{Emit => GenerateCS}/ModulesTest.cs | 24 +- .../{Emit => GenerateCS}/SerializerTest.cs | 2 +- .../{Pack => GenerateJS}/DeclarationTest.cs | 865 +++++++++-------- .../GenerateJSTest.cs} | 11 +- .../InspectionTest.cs} | 2 +- .../GenerateJS/JSInstanceTest.cs | 259 +++++ .../GenerateJS/JSModuleTest.cs | 805 ++++++++++++++++ .../GenerateJS/JSSerializerTest.cs | 114 +++ .../{Pack => GenerateJS}/ResourceTest.cs | 2 +- .../Pack/BindingTest.cs | 905 ------------------ src/cs/Bootsharp.Publish.Test/TaskTest.cs | 44 +- .../Bootsharp.Publish/Common/CodeBuilder.cs | 71 ++ .../Common/Global/GlobalInspection.cs | 33 +- .../Common/Global/GlobalText.cs | 18 +- .../Common/Global/GlobalType.cs | 41 +- .../InspectionReporter.cs | 8 +- .../Meta/DocMeta.cs} | 2 +- .../{ => Inspection}/Meta/InteropKind.cs | 0 .../{ => Inspection}/Meta/MemberMeta.cs | 53 +- .../{ => Inspection}/Meta/SerializedMeta.cs | 22 +- .../Common/Inspection/Meta/SurfaceMeta.cs | 87 ++ .../Common/Inspection/Meta/TypeMeta.cs | 29 + .../Common/{ => Inspection}/Meta/ValueMeta.cs | 2 +- .../SerializedInspector.cs | 28 +- .../Common/Inspection/SolutionInspection.cs | 30 + .../Common/Inspection/SolutionInspector.cs | 51 + .../Common/Inspection/TypeInspector.cs | 201 ++++ .../Common/Inspector/InstancedInspector.cs | 99 -- .../Common/Inspector/MemberInspector.cs | 59 -- .../Common/Inspector/SolutionInspection.cs | 44 - .../Common/Inspector/SolutionInspector.cs | 117 --- .../Common/Meta/InstancedMeta.cs | 40 - .../Common/Meta/ModuleMeta.cs | 29 - .../Bootsharp.Publish/Common/Meta/TypeMeta.cs | 16 - .../Common/Preferences/Preferences.cs | 12 +- .../Common/Preferences/PreferencesResolver.cs | 25 +- .../Emit/InstanceGenerator.cs | 121 --- .../Emit/InteropGenerator.cs | 247 ----- .../GenerateCS.cs} | 36 +- .../GenerateCS/InstanceGenerator.cs | 129 +++ .../GenerateCS/InteropGenerator.cs | 230 +++++ .../{Emit => GenerateCS}/ModuleGenerator.cs | 67 +- .../SerializerGenerator.cs | 123 ++- .../Declarations/DeclarationGenerator.cs | 166 ++++ .../Declarations}/DocumentationBuilder.cs | 72 +- .../Declarations}/TypeSyntaxBuilder.cs | 18 +- .../DotNetPatcher.cs} | 2 +- .../GenerateJS/GenerateJS.cs | 103 ++ .../GenerateJS/JSImportsGenerator.cs | 19 + .../GenerateJS/JSInstanceGenerator.cs | 90 ++ .../GenerateJS/JSModuleGenerator.cs | 190 ++++ .../GenerateJS/JSModules/JSModule.cs | 52 + .../GenerateJS/JSModules/JSModules.cs | 57 ++ .../GenerateJS/JSModules/JSNode.cs | 43 + .../GenerateJS/JSSerializerGenerator.cs | 84 ++ .../{Pack => GenerateJS}/ResourceGenerator.cs | 0 .../BindingGenerator/BindingClassGenerator.cs | 49 - .../Pack/BindingGenerator/BindingGenerator.cs | 328 ------- .../BindingSerializerGenerator.cs | 90 -- .../Bootsharp.Publish/Pack/BootsharpPack.cs | 85 -- .../DeclarationGenerator.cs | 13 - .../ModuleDeclarationGenerator.cs | 109 --- .../TypeDeclarationGenerator.cs | 190 ---- src/cs/Bootsharp/Build/Bootsharp.targets | 43 +- src/cs/Bootsharp/Build/PackageTemplate.json | 3 +- src/cs/Directory.Build.props | 2 +- src/js/package.json | 2 +- src/js/scripts/build.sh | 1 - src/js/src/exports.mts | 15 + src/js/src/generated/bindings.g.mts | 1 - src/js/src/generated/imports.g.mts | 2 + src/js/src/generated/index.g.mts | 1 + src/js/src/imports.mts | 27 +- src/js/src/index.mts | 2 +- src/js/src/instances.mts | 35 +- src/js/src/serialization.mts | 442 --------- src/js/src/serialization/index.mts | 12 + src/js/src/serialization/reader.mts | 111 +++ src/js/src/serialization/serializer.mts | 32 + src/js/src/serialization/std.mts | 166 ++++ src/js/src/serialization/writer.mts | 140 +++ src/js/test/cs.ts | 23 +- .../cs/Test.Library/Modules/Bidirectional.cs | 12 + .../cs/Test.Library/Modules/IBidirectional.cs | 12 + .../test/cs/Test.Library/Modules/Modules.cs | 24 +- .../cs/Test.Library/Registries/Registries.cs | 1 + .../cs/Test.Library/Registries/Registry.cs | 9 + src/js/test/cs/Test/package.json | 3 +- src/js/test/spec/boot.spec.ts | 13 +- src/js/test/spec/interop.spec.ts | 200 ++-- src/js/test/spec/platform.spec.ts | 15 +- src/js/test/spec/serialization.spec.ts | 101 +- 110 files changed, 4799 insertions(+), 4133 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/cs/Bootsharp.Common/Interop/JSProxy.cs rename src/cs/Bootsharp.Publish.Test/{Emit/EmitTest.cs => GenerateCS/GenerateCSTest.cs} (87%) rename src/cs/Bootsharp.Publish.Test/{Emit => GenerateCS}/InstancesTest.cs (77%) rename src/cs/Bootsharp.Publish.Test/{Emit => GenerateCS}/InteropTest.cs (58%) rename src/cs/Bootsharp.Publish.Test/{Emit => GenerateCS}/ModulesTest.cs (89%) rename src/cs/Bootsharp.Publish.Test/{Emit => GenerateCS}/SerializerTest.cs (99%) rename src/cs/Bootsharp.Publish.Test/{Pack => GenerateJS}/DeclarationTest.cs (71%) rename src/cs/Bootsharp.Publish.Test/{Pack/PackTest.cs => GenerateJS/GenerateJSTest.cs} (82%) rename src/cs/Bootsharp.Publish.Test/{Pack/SolutionInspectionTest.cs => GenerateJS/InspectionTest.cs} (98%) create mode 100644 src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs create mode 100644 src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs create mode 100644 src/cs/Bootsharp.Publish.Test/GenerateJS/JSSerializerTest.cs rename src/cs/Bootsharp.Publish.Test/{Pack => GenerateJS}/ResourceTest.cs (97%) delete mode 100644 src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs create mode 100644 src/cs/Bootsharp.Publish/Common/CodeBuilder.cs rename src/cs/Bootsharp.Publish/Common/{Inspector => Inspection}/InspectionReporter.cs (77%) rename src/cs/Bootsharp.Publish/Common/{Meta/DocumentationMeta.cs => Inspection/Meta/DocMeta.cs} (79%) rename src/cs/Bootsharp.Publish/Common/{ => Inspection}/Meta/InteropKind.cs (100%) rename src/cs/Bootsharp.Publish/Common/{ => Inspection}/Meta/MemberMeta.cs (68%) rename src/cs/Bootsharp.Publish/Common/{ => Inspection}/Meta/SerializedMeta.cs (88%) create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/Meta/TypeMeta.cs rename src/cs/Bootsharp.Publish/Common/{ => Inspection}/Meta/ValueMeta.cs (96%) rename src/cs/Bootsharp.Publish/Common/{Inspector => Inspection}/SerializedInspector.cs (89%) create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspection.cs create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspector.cs create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs delete mode 100644 src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs rename src/cs/Bootsharp.Publish/{Emit/BootsharpEmit.cs => GenerateCS/GenerateCS.cs} (65%) create mode 100644 src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs rename src/cs/Bootsharp.Publish/{Emit => GenerateCS}/ModuleGenerator.cs (56%) rename src/cs/Bootsharp.Publish/{Emit => GenerateCS}/SerializerGenerator.cs (51%) create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs rename src/cs/Bootsharp.Publish/{Pack/DeclarationGenerator => GenerateJS/Declarations}/DocumentationBuilder.cs (59%) rename src/cs/Bootsharp.Publish/{Pack/DeclarationGenerator => GenerateJS/Declarations}/TypeSyntaxBuilder.cs (93%) rename src/cs/Bootsharp.Publish/{Pack/ModulePatcher.cs => GenerateJS/DotNetPatcher.cs} (97%) create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/GenerateJS.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModule.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSNode.cs create mode 100644 src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs rename src/cs/Bootsharp.Publish/{Pack => GenerateJS}/ResourceGenerator.cs (100%) delete mode 100644 src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs delete mode 100644 src/js/src/generated/bindings.g.mts create mode 100644 src/js/src/generated/imports.g.mts create mode 100644 src/js/src/generated/index.g.mts delete mode 100644 src/js/src/serialization.mts create mode 100644 src/js/src/serialization/index.mts create mode 100644 src/js/src/serialization/reader.mts create mode 100644 src/js/src/serialization/serializer.mts create mode 100644 src/js/src/serialization/std.mts create mode 100644 src/js/src/serialization/writer.mts create mode 100644 src/js/test/cs/Test.Library/Modules/Bidirectional.cs create mode 100644 src/js/test/cs/Test.Library/Modules/IBidirectional.cs create mode 100644 src/js/test/cs/Test.Library/Registries/Registry.cs diff --git a/.gitignore b/.gitignore index 9c7bdde1..3a196e81 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ obj *.user *.nupkg package-lock.json -last-failed-test-dump.txt \ No newline at end of file +last-failed-test-dump.txt diff --git a/AGENTS.md b/AGENTS.md index 7af9ddf2..30edde09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,16 +1,24 @@ # Requirements -- Keep the code lean and efficient, including the use of `unsafe` when it is justified. -- Use the latest available .NET and C# features when they improve the code and fit the existing style. -- Avoid defensive programming and compatibility overhead. Target only the modern WASM runtime, current JS specs and current browser capabilities. -- Follow the existing code style, architecture, project structure, naming and formatting strictly. +- Follow the existing code style, architecture, naming and formatting strictly. +- Use the latest C# features when they fit the existing style. +- Avoid defensive programming and compatibility overhead. - If clarification is required, use the question tool instead of guessing. -IMPORTANT: NEVER RUN ANY BUILD/PUBLISH COMMANDS IN PARALLEL. +# Export-Import Model + +We have "export" and "import" concepts used throughout the codebase. The model is always C#-centric and means the same thing on both the C# and JavaScript sides: + +- Export: something in C# being exported to JavaScript +- Import: something in JavaScript being imported to C# + +For example, an exported method means a C# method exposed to JavaScript, and we refer to it as exported in both the C# and JavaScript code. An imported method means the opposite: a JavaScript function bound to a partial C# method, referred to as imported in both C# and JS code. + +Make sure to follow this convention strictly. # Packaging Bootsharp -Follow these steps exactly and sequentially whenever the Bootsharp package consumed by other projects must be actualized, or when running the JS end-to-end tests after updating the package's C# or JS code. +Follow these steps exactly and sequentially whenever the Bootsharp package consumed by other projects must be actualized, or when running the JS end-to-end tests after modifying the package's C# or JS code. 1. Build the JS package with `npm run build` under `src/js`. 2. Bump the Bootsharp library alpha version in `src/cs/Directory.Build.props` @@ -30,7 +38,7 @@ We have a strict 100% coverage policy for both the C# and JS codebases. - Tests must be meaningful and cover real behavior. - Do not add fake tests just to satisfy the numbers. -- No unreachable code is allowed, except in rare cases where testing is not practical. In those cases, ask how to proceed. +- No unreachable code is allowed, except in rare cases where testing is not practical. - Treat branch coverage as part of the requirement, not just line coverage. To check C# coverage, use `reportgenerator` on merged coverlet output. Example workflow reference: `src/cs/.scripts/cover.sh`. Do not run that script verbatim in automation; it is intended for interactive usage. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index 81195de4..209aa8a4 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -1,87 +1,91 @@ # Type Declarations -Bootsharp will automatically generate [type declarations](https://www.typescriptlang.org/docs/handbook/2/type-declarations) for interop APIs when building the solution. The files are emitted under "types" 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/` directory of the compiled module package. ## Function Declarations -For the interop methods, function declarations are emitted. - -Exported methods will have associated function assigned under the declaring type space: +For interop methods, function declarations are emitted under the class's TS namespace wrapper inside the C# namespace's module: ```csharp -public class Foo +namespace Foo; + +public class Bar { [Export] - public static void Bar() { } + public static void Baz() { } } ``` -— will make following emitted in the declaration file: +— will make the following emitted in `generated/foo.g.d.mts`: ```ts -export namespace Foo { - export function bar(): void; +export namespace Bar { + export function baz(): void; } ``` -— which allows consuming the API in JavaScript as follows: +— which allows consuming the API in JavaScript as: ```ts -import { Foo } from "bootsharp"; +import { Bar } from "bootsharp/foo"; -Foo.bar(); +Bar.baz(); ``` Imported methods will be emitted as properties, which have to be assigned before booting the runtime: ::: code-group -```csharp [Foo.cs] -public partial class Foo +```csharp [Bar.cs] +namespace Foo; + +public partial class Bar { [Import] - public static partial void Bar(); + public static partial void Baz(); } ``` -```ts [bindings.d.ts] -export namespace Foo { - export let bar: () => void; +```ts [foo.g.d.mts] +export namespace Bar { + export let baz: () => void; } ``` ```ts [main.ts] -import { Foo } from "bootsharp"; +import { Bar } from "bootsharp/foo"; -Foo.bar = () => {}; +Bar.baz = () => {}; ``` ::: ## Property Declarations -Exported properties are emitted as variables under the declaring type space: +Exported properties are emitted as variables under the declaring class's TS namespace: ::: code-group -```csharp [Foo.cs] -public class Foo +```csharp [Bar.cs] +namespace Foo; + +public class Bar { [Export] - public static string Bar { get; set; } = ""; + public static string Baz { get; set; } = ""; } ``` -```ts [bindings.d.ts] -export namespace Foo { - export let bar: string; +```ts [foo.g.d.mts] +export namespace Bar { + export let baz: string; } ``` ```ts [main.ts] -import { Foo } from "bootsharp"; +import { Bar } from "bootsharp/foo"; -Foo.bar = "updated"; +Bar.baz = "updated"; ``` ::: @@ -90,25 +94,27 @@ Imported properties are emitted as accessor pairs, which have to be assigned bef ::: code-group -```csharp [Foo.cs] -public static partial class Foo +```csharp [Bar.cs] +namespace Foo; + +public static partial class Bar { [Import] - public static partial string Bar { get; set; } + public static partial string Baz { get; set; } } ``` -```ts [bindings.d.ts] -export namespace Foo { - export let bar: { get: () => string; set: (value: string) => void }; +```ts [foo.g.d.mts] +export namespace Bar { + export let baz: { get: () => string; set: (value: string) => void }; } ``` ```ts [main.ts] -import { Foo } from "bootsharp"; +import { Bar } from "bootsharp/foo"; -let bar = ""; -Foo.bar = { get: () => bar, set: value => bar = value }; +let baz = ""; +Bar.baz = { get: () => baz, set: value => baz = value }; ``` ::: @@ -119,24 +125,26 @@ Exported events are emitted as `EventSubscriber` objects: ::: code-group -```csharp [Foo.cs] -public class Foo +```csharp [Bar.cs] +namespace Foo; + +public class Bar { [Export] - public static event Action? OnBar; + public static event Action? OnBaz; } ``` -```ts [bindings.d.ts] -export namespace Foo { - export const onBar: EventSubscriber<[payload: string]>; +```ts [foo.g.d.mts] +export namespace Bar { + export const onBaz: EventSubscriber<[payload: string]>; } ``` ```ts [main.ts] -import { Foo } from "bootsharp"; +import { Bar } from "bootsharp/foo"; -Foo.onBar.subscribe(payload => {}); +Bar.onBaz.subscribe(payload => {}); ``` ::: @@ -145,24 +153,26 @@ Imported events are emitted as `EventBroadcaster` objects: ::: code-group -```csharp [Foo.cs] -public static partial class Foo +```csharp [Bar.cs] +namespace Foo; + +public static partial class Bar { [Import] - public static event Action? OnBar; + public static event Action? OnBaz; } ``` -```ts [bindings.d.ts] -export namespace Foo { - export const onBar: EventBroadcaster<[payload: string]>; +```ts [foo.g.d.mts] +export namespace Bar { + export const onBaz: EventBroadcaster<[payload: string]>; } ``` ```ts [main.ts] -import { Foo } from "bootsharp"; +import { Bar } from "bootsharp/foo"; -Foo.onBar.broadcast("updated"); +Bar.onBaz.broadcast("updated"); ``` ::: @@ -173,7 +183,9 @@ When an inspected assembly has XML documentation generated, Bootsharp mirrors th ::: code-group -```csharp [Foo.cs] +```csharp [MathApi.cs] +namespace Foo; + /// Math API. public class MathApi { @@ -186,7 +198,7 @@ public class MathApi } ``` -```ts [bindings.d.ts] +```ts [foo.g.d.mts] /** * Math API. */ @@ -216,28 +228,32 @@ This is intentional and optimized for TypeScript ergonomics. Refer to the dedica ## Type Crawling -Bootsharp will crawl types from the interop signatures and mirror them in the emitted declarations. For example, if you have a custom record with property of another custom record implementing a custom interface, both records and the interface will be emitted: +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: ::: code-group ```csharp [Foo.cs] +namespace Space; + public interface IFoo { }; public record Foo : IFoo; public record Bar (Foo foo); -public partial class Foo +public partial class Holder { [Import] public static partial Bar GetBar(); } ``` -```ts [bindings.d.ts] +```ts [space.g.d.mts] export interface IFoo {} -export interface Foo implements IFoo {} -export interface Bar {foo: Foo;} +export type Foo = IFoo & Readonly<{}>; +export type Bar = Readonly<{ + foo: Foo; +}>; -export namespace Foo { +export namespace Holder { export function getBar(): Bar; } ``` @@ -246,4 +262,4 @@ export namespace Foo { ## Configuring Type Mappings -You can override which type declaration are generated for associated C# types via `Type` patterns of [emit preferences](/guide/preferences). +You can override which type declaration is generated for associated C# types via `Type` patterns of [emit preferences](/guide/preferences). diff --git a/docs/guide/namespaces.md b/docs/guide/namespaces.md index b411fc90..9e3fedbe 100644 --- a/docs/guide/namespaces.md +++ b/docs/guide/namespaces.md @@ -1,10 +1,12 @@ # Namespaces -Bootsharp maps binding APIs based on the fully qualified name of the C# types. +Bootsharp projects each C# namespace into its own ES module. The full namespace becomes the import path; individual classes, enums and interface bindings inside that namespace become flat top-level exports of that module. + +The slug rule is: PascalCase → kebab-case, dot → directory separator. `Foo.Bar` → `foo/bar`. `MyRootSpace.MyOtherSpace` → `my-root-space/my-other-space`. ## Static Members -Full type name (including namespace) of the declaring type of the static member is mapped into JavaScript object name: +The C# namespace of the declaring type maps to a sub-path under the Bootsharp module; the declaring class becomes a flat `export const`: ```csharp class Class { [Export] static void Method() {} } @@ -13,14 +15,18 @@ namespace Foo.Bar { class Class { [Export] static void Method() {} } } ``` ```ts -import { Class, Foo } from "bootsharp"; +import { Class as Root } from "bootsharp"; // root-namespace members re-exported from the entry +import { Class as FooClass } from "bootsharp/foo"; +import { Class as FooBarClass } from "bootsharp/foo/bar"; -Class.method(); -Foo.Class.method(); -Foo.Bar.Class.method(); +Root.method(); +FooClass.method(); +FooBarClass.method(); ``` -Methods inside nested classes are treated as if they were declared under namespace: +Bindings declared without any C# namespace live in `bootsharp/index` and are re-exported from the package entry, so root-namespace types remain importable directly from `"bootsharp"`. + +Methods inside nested classes are emitted under the containing class's binding inside the namespace's module file: ```csharp namespace Foo; @@ -32,14 +38,14 @@ public class Class ``` ```ts -import { Foo } from "bootsharp"; +import { Class } from "bootsharp/foo"; -Foo.Class.Nested.method(); +Class.Nested.method(); ``` ## Interop Modules -When generating bindings for [modules](/guide/interop-modules), an interface name is assumed to have an "I" prefix, so the associated JavaScript name will have the first character removed. Class modules keep their name as-is. In either case, if the type is declared under a namespace, it'll be mirrored in JavaScript. +When generating bindings for [modules](/guide/interop-modules), the JS export uses the C# type name as-is. The C# namespace maps to the import path the same way it does for static members: ```csharp [Export( @@ -54,16 +60,18 @@ namespace Foo.Bar { class Exported { public void Method() {} } } ``` ```ts -import { Exported, Foo } from "bootsharp"; +import { IExported as Root } from "bootsharp"; +import { IExported as FooExported } from "bootsharp/foo"; +import { Exported as FooBarExported } from "bootsharp/foo/bar"; -Exported.method(); -Foo.Exported.method(); -Foo.Bar.Exported.method(); +Root.method(); +FooExported.method(); +FooBarExported.method(); ``` ## Types -Custom types referenced in API signatures (records, classes, interfaces, etc) are declared under their respective namespace when they have one, or under root otherwise. +Custom types referenced in API signatures (records, classes, interfaces, etc) are declared as top-level exports of their respective namespace module: ```csharp public record Record; @@ -77,15 +85,16 @@ partial class Class ``` ```ts -import { Class, Record, Foo } from "bootsharp"; +import { Class, type Record } from "bootsharp"; +import type { Record as FooRecord } from "bootsharp/foo"; Class.method = methodImpl; -function methodImpl(r: Record): Foo.Record { - +function methodImpl(r: Record): FooRecord { + // ... } ``` ## Configuring Namespaces -You can control how namespaces are generated with `Space` option in [preferences](/guide/preferences). +You can control how the C#-side namespace path resolves to the generated module path with the `Space` option in [preferences](/guide/preferences). A pref that rewrites `Foo.Bar.SomeClass` to `Bar.NewClass` will emit the binding into `bootsharp/bar` under the name `NewClass`. diff --git a/docs/guide/preferences.md b/docs/guide/preferences.md index 0761c09e..a6076619 100644 --- a/docs/guide/preferences.md +++ b/docs/guide/preferences.md @@ -14,11 +14,7 @@ To customize emitted spaces, use `Space` parameter. For example, to make all bin )] ``` -The patterns are matched against full type name of declaring C# type when generating JavaScript objects for interop methods and against namespace when generating TypeScript syntax for C# types. Matched type names have the following modifications: - -- interfaces have first character removed -- generics have parameter spec removed -- nested type names have `+` replaced with `.` +The patterns are matched against the C# full type name. Nested types use `+` as separator; generic types include the arity suffix. ## Type diff --git a/samples/react/src/computer.tsx b/samples/react/src/computer.tsx index ff689f29..35caac42 100644 --- a/samples/react/src/computer.tsx +++ b/samples/react/src/computer.tsx @@ -1,8 +1,8 @@ import { useEffect, useState, useCallback, ChangeEvent } from "react"; -import { Computer } from "backend"; +import { Computer, type Options } from "backend"; type Props = { - options: Computer.Options; + options: Options; resultLimit: number; }; diff --git a/src/cs/Bootsharp.Common.Test/InstancesTest.cs b/src/cs/Bootsharp.Common.Test/InstancesTest.cs index 81682021..3bc1c3ef 100644 --- a/src/cs/Bootsharp.Common.Test/InstancesTest.cs +++ b/src/cs/Bootsharp.Common.Test/InstancesTest.cs @@ -4,8 +4,14 @@ namespace Bootsharp.Common.Test; public class InstancesTest { + private interface IFoo; + private interface IBar; + private class Foo : IFoo; + private class Bar : IBar; + private class Proxy (int id) : JSProxy(id); + [Fact] - public void CaneExportAndDisposeInstance () + public void CanExportAndDisposeInstance () { var exported = new object(); var id = Export(exported); @@ -20,12 +26,18 @@ public void GeneratesUniqueIdsForUniqueExports () } [Fact] - public void KeepsStableIdsForSameExports () + public void ShortCircuitsRegisteredExports () { var exported = new object(); Assert.Equal(Export(exported), Export(exported)); } + [Fact] + public void ShortCircuitsImportedProxies () + { + Assert.Equal(42, Export(new Proxy(42))); + } + [Fact] public void InvokesExportFactoryCallbacks () { @@ -44,24 +56,34 @@ public void InvokesExportFactoryCallbacks () [Fact] public void CanImportAndDisposeInstance () { - var imported = new object(); - Assert.Same(imported, Import(1, _ => imported)); + var imported = new Foo(); + RegisterImport(typeof(IFoo), _ => imported); + Assert.Same(imported, Resolve(1)); DisposeImported(1); } [Fact] - public void CachesImportsUntilDisposed () + public void ShortCircuitsRegisteredImportsUntilDisposed () { - var imported = new object(); - Import(42, _ => imported); + var imported = new Bar(); + RegisterImport(typeof(IBar), _ => imported); + Resolve(42); + RegisterImport(typeof(IBar), _ => new Bar()); Assert.Same(imported, - // We don't use the factory here and ignore the fact that it returns another instance, - // because we already have previous import associated with the '42' ID — it's the - // responsibility of the JS side to let us know when the instance is disposed. - Import(42, _ => new object())); + // We already have previous import associated with the '42' ID — the registered + // factory should not be invoked again; it's the responsibility of the JS side + // to let us know when the instance is disposed. + Resolve(42)); // Here, we simulate JS side telling us to dispose the '42' instance. DisposeImported(42); // Now we exercise the factory and register the new instance as '42'. - Assert.NotSame(imported, Import(42, _ => new object())); + Assert.NotSame(imported, Resolve(42)); + } + + [Fact] + public void ShortCircuitsImportedExports () + { + var exported = new object(); + Assert.Same(exported, Resolve(Export(exported))); } } diff --git a/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs b/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs index 7123a274..9a4160e3 100644 --- a/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs @@ -20,31 +20,38 @@ namespace Bootsharp; public sealed class PreferencesAttribute : Attribute { /// - /// Customize generated JavaScript object names and TypeScript namespaces. + /// Customize how C# type namespaces transform into JavaScript module names. /// /// - /// The patterns are matched against full type name (namespace.typename) of - /// declaring C# type when generating JavaScript objects for interop methods - /// and against namespace when generating TypeScript syntax for C# types. - /// Matched type names have the following modifications:
- /// - interfaces have first character removed
- /// - generics have parameter spec removed
- /// - nested have "+" replaced with "."
+ /// The patterns are matched against the C# type namespace, or 'index' when the type is global. ///
public string[] Space { get; init; } = []; /// - /// Customize generated TypeScript type syntax. + /// Customize how C# type names transform into JavaScript object names. /// /// - /// The patterns are matched against full C# type names of - /// interop method arguments, return values and object properties. + /// The patterns are matched against the C# type names, with generic identity removed. /// - public string[] Type { get; init; } = []; + public string[] Name { get; init; } = []; /// - /// Customize generated JavaScript function names. + /// Customize how C# method names transform into JavaScript function names. /// /// - /// The patterns are matched against C# interop method names. + /// The patterns are matched against the C# reflected method names. /// - public string[] Function { get; init; } = []; + public string[] Method { get; init; } = []; + /// + /// Customize how C# property names transform into JavaScript property names. + /// + /// + /// The patterns are matched against the C# reflected property names. + /// + public string[] Property { get; init; } = []; + /// + /// Customize how C# event names transform into JavaScript event names. + /// + /// + /// The patterns are matched against the C# reflected event names. + /// + public string[] Event { get; init; } = []; } diff --git a/src/cs/Bootsharp.Common/Interop/Instances.cs b/src/cs/Bootsharp.Common/Interop/Instances.cs index 659444d6..a5485e1c 100644 --- a/src/cs/Bootsharp.Common/Interop/Instances.cs +++ b/src/cs/Bootsharp.Common/Interop/Instances.cs @@ -1,36 +1,59 @@ namespace Bootsharp; /// -/// Manages exported (C# -> JavaScript) and imported (JavaScript -> C#) instanced interop interfaces. +/// Manages exported (C# -> JavaScript) and imported (JavaScript -> C#) instances. /// /// /// Exported instances originate from C# and are created in the user code; /// we track them to be able to invoke their members in the static bindings. -/// Imported instances originate from JS and are represented with the C# wrappers generated by Bootsharp; +/// Imported instances originate from JS and are represented with the C# proxies generated by Bootsharp; /// we track them to be able to invoke their event raisers in the static bindings. /// public static class Instances { + /// + /// Invoked on when registering the instance. + /// + /// The unique identifier of the instance. + /// The registered instance. + /// The callback to invoke when disposing the instance. + public delegate Action ExportCallback (int id, T instance) where T : class; + private static readonly Dictionary importedById = []; + private static readonly Dictionary> importers = []; private static readonly Dictionary exportedById = []; private static readonly Dictionary idByExported = new(ReferenceEqualityComparer.Instance); private static readonly Dictionary onDisposeById = []; private static readonly Queue idPool = []; - private static int nextId = int.MinValue; + private static int nextId = int.MinValue; // C# IDs are always negative; JS's — positive. + + /// + /// Resolves a registered instance associated with the specified ID, or uses a factory that + /// was registered with to register a new imported instance. + /// + public static T Resolve (int id) where T : class + { + if (id < 0) return (T)exportedById[id]; + if (importedById.GetValueOrDefault(id) is { } weak) return (T)weak.Target!; + var instance = (T)importers[typeof(T)](id); + importedById[id] = new(instance); + return instance; + } /// - /// Registers specified exported instance and associates it with a unique ID, unless it's already registered, - /// in which case the ID of the registered instance is returned. + /// Registers specified exported (C#) instance and returns the associated unique ID. + /// Short-circuits already registered exported and imported instances. /// /// The instance to register. - /// Callbacks to invoke when registering and disposing the instance. + /// Callback to invoke when registering and disposing the instance. /// Unique ID associated with the registered instance. - public static int Export (T instance, Func? factory = 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 (idByExported.TryGetValue(instance, out var id)) return id; id = idPool.Count > 0 ? idPool.Dequeue() : nextId++; exportedById[idByExported[instance] = id] = instance; - if (factory != null) onDisposeById[id] = factory(id, instance); + if (cb != null) onDisposeById[id] = cb(id, instance); return id; } @@ -42,18 +65,6 @@ public static T Exported (int id) where T : class return (T)exportedById[id]; } - /// - /// Invokes the specified factory to create and register an imported instance wrapper associated with the ID, - /// unless an imported instance is already registered under the ID, in which case returns its wrapper. - /// - public static T Import (int id, Func factory) where T : class - { - if (importedById.GetValueOrDefault(id) is { } weak) return (T)weak.Target!; - var instance = factory(id); - importedById[id] = new(instance); - return instance; - } - /// /// Notifies that an exported instance with the specified ID is no longer used on the JavaScript side /// (eg, was garbage collected) and can be released on the C# side as well. @@ -65,6 +76,14 @@ public static void DisposeExported (int id) idPool.Enqueue(id); } + /// + /// Registers a factory for creating binding proxy of a JS-originated instance of the specified type. + /// + public static void RegisterImport (Type type, Func factory) + { + importers[type] = factory; + } + /// /// Notifies that an imported interop instance with the specified ID is no longer used /// on the C# side and can be untracked. diff --git a/src/cs/Bootsharp.Common/Interop/JSProxy.cs b/src/cs/Bootsharp.Common/Interop/JSProxy.cs new file mode 100644 index 00000000..c9c661cd --- /dev/null +++ b/src/cs/Bootsharp.Common/Interop/JSProxy.cs @@ -0,0 +1,12 @@ +namespace Bootsharp; + +/// +/// Base class for generated proxies used to bind JS-originated instances. +/// +public abstract class JSProxy (int id) +{ + /// + /// Unique identifier of the proxied JS instance. + /// + protected internal readonly int _id = id; +} diff --git a/src/cs/Bootsharp.Common/Interop/Modules.cs b/src/cs/Bootsharp.Common/Interop/Modules.cs index 21e52764..05d19646 100644 --- a/src/cs/Bootsharp.Common/Interop/Modules.cs +++ b/src/cs/Bootsharp.Common/Interop/Modules.cs @@ -15,7 +15,7 @@ public static class Modules { /// /// Export modules metadata generated for types (classes or interfaces) specified under - /// mapped by the generated wrapper type. + /// mapped by the associated proxy type. /// public static IReadOnlyDictionary Exports => exports; /// @@ -28,10 +28,10 @@ public static class Modules private static readonly Dictionary imports = new(); /// - /// Maps type of the generated export module wrapper to the associated export module metadata. + /// Maps type of the generated export module proxy to the associated export module metadata. /// Invoked by the generated code before program start. /// - public static void Register (Type wrapper, ExportModule export) => exports[wrapper] = export; + public static void Register (Type proxy, ExportModule export) => exports[proxy] = export; /// /// Maps interface type of the imported module to the associated import module metadata. /// Invoked by the generated code before program start. diff --git a/src/cs/Bootsharp.Common/Interop/Proxies.cs b/src/cs/Bootsharp.Common/Interop/Proxies.cs index 9e87ae5a..0b753a19 100644 --- a/src/cs/Bootsharp.Common/Interop/Proxies.cs +++ b/src/cs/Bootsharp.Common/Interop/Proxies.cs @@ -28,7 +28,7 @@ namespace Bootsharp; /// public static class Proxies { - private static readonly Dictionary map = new(StringComparer.Ordinal); + private static readonly Dictionary map = []; /// /// Maps specified interop delegate to the specified ID. diff --git a/src/cs/Bootsharp.Generate.Test/ImportPropertyTest.cs b/src/cs/Bootsharp.Generate.Test/ImportPropertyTest.cs index 6a2ec54f..53f9d4c3 100644 --- a/src/cs/Bootsharp.Generate.Test/ImportPropertyTest.cs +++ b/src/cs/Bootsharp.Generate.Test/ImportPropertyTest.cs @@ -14,9 +14,9 @@ partial class Foo """ unsafe partial class Foo { - static partial global::System.Int32 Counter { get => Bootsharp_GetPropertyCounter(); set => Bootsharp_SetPropertyCounter(value); } - public static delegate* managed Bootsharp_GetPropertyCounter; - public static delegate* managed Bootsharp_SetPropertyCounter; + static partial global::System.Int32 Counter { get => Bootsharp_GetCounter(); set => Bootsharp_SetCounter(value); } + public static delegate* managed Bootsharp_GetCounter; + public static delegate* managed Bootsharp_SetCounter; } """ }, @@ -35,8 +35,8 @@ namespace Space; public static unsafe partial class Foo { - public static partial global::System.String Label { get => Bootsharp_GetPropertyLabel(); } - public static delegate* managed Bootsharp_GetPropertyLabel; + public static partial global::System.String Label { get => Bootsharp_GetLabel(); } + public static delegate* managed Bootsharp_GetLabel; } """ }, @@ -51,8 +51,8 @@ partial class Foo """ unsafe partial class Foo { - static partial global::System.Boolean Active { set => Bootsharp_SetPropertyActive(value); } - public static delegate* managed Bootsharp_SetPropertyActive; + static partial global::System.Boolean Active { set => Bootsharp_SetActive(value); } + public static delegate* managed Bootsharp_SetActive; } """ }, @@ -68,9 +68,9 @@ partial class Foo """ unsafe partial class Foo { - static partial global::System.Int32 Counter { get => Bootsharp_GetPropertyCounter(); set => Bootsharp_SetPropertyCounter(value); } - public static delegate* managed Bootsharp_GetPropertyCounter; - public static delegate* managed Bootsharp_SetPropertyCounter; + static partial global::System.Int32 Counter { get => Bootsharp_GetCounter(); set => Bootsharp_SetCounter(value); } + public static delegate* managed Bootsharp_GetCounter; + public static delegate* managed Bootsharp_SetCounter; } """ } diff --git a/src/cs/Bootsharp.Generate/ImportProperty.cs b/src/cs/Bootsharp.Generate/ImportProperty.cs index 5ef1c717..5bd1c95f 100644 --- a/src/cs/Bootsharp.Generate/ImportProperty.cs +++ b/src/cs/Bootsharp.Generate/ImportProperty.cs @@ -11,10 +11,10 @@ public string EmitSource (Compilation cmp) var type = BuildSyntax(p.Type); var canGet = p.GetMethod != null; var canSet = p.SetMethod != null; - var get = canGet ? $"get => Bootsharp_GetProperty{p.Name}(); " : ""; - var set = canSet ? $"set => Bootsharp_SetProperty{p.Name}(value); " : ""; - var getter = canGet ? $"\n public static delegate* managed<{type}> Bootsharp_GetProperty{p.Name};" : ""; - var setter = canSet ? $"\n public static delegate* managed<{type}, void> Bootsharp_SetProperty{p.Name};" : ""; + var get = canGet ? $"get => Bootsharp_Get{p.Name}(); " : ""; + var set = canSet ? $"set => Bootsharp_Set{p.Name}(value); " : ""; + var getter = canGet ? $"\n public static delegate* managed<{type}> Bootsharp_Get{p.Name};" : ""; + var setter = canSet ? $"\n public static delegate* managed<{type}, void> Bootsharp_Set{p.Name};" : ""; return $"{stx.Modifiers} {type} {p.Name} {{ {get}{set}}}{getter}{setter}"; } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/GenerateCSTest.cs similarity index 87% rename from src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/GenerateCSTest.cs index ead128b2..898b58f3 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/GenerateCSTest.cs @@ -1,8 +1,8 @@ namespace Bootsharp.Publish.Test; -public class EmitTest : TaskTest +public class GenerateCSTest : TaskTest { - protected BootsharpEmit Task { get; } + protected GenerateCS Task { get; } protected string GeneratedSerializer => ReadProjectFile(serializerPath); protected string GeneratedInstances => ReadProjectFile(instancesPath); protected string GeneratedModules => ReadProjectFile(modulesPath); @@ -13,7 +13,7 @@ public class EmitTest : TaskTest private string modulesPath => $"{Project.Root}/Modules.g.cs"; private string interopPath => $"{Project.Root}/Interop.g.cs"; - public EmitTest () + public GenerateCSTest () { Task = CreateTask(); } @@ -25,7 +25,7 @@ public override void Execute () Task.Execute(); } - private BootsharpEmit CreateTask () => new() { + private GenerateCS CreateTask () => new() { InspectedDirectory = Project.Root, EntryAssemblyName = "System.Runtime.dll", SerializerFilePath = serializerPath, diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs similarity index 77% rename from src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs index 201150f7..c8079b48 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/InstancesTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class InstancesTest : EmitTest +public class InstancesTest : GenerateCSTest { protected override string TestedContent => GeneratedInstances; @@ -33,10 +33,8 @@ public class Class """ namespace Bootsharp.Generated.Imports { - public class JSImported (global::System.Int32 id) : global::IImported + public class JSImported (int id) : Bootsharp.JSProxy(id), global::IImported { - internal readonly global::System.Int32 _id = id; - ~JSImported() => Instances.DisposeImported(_id); public event global::System.Action OnRecordChanged; @@ -45,8 +43,8 @@ public class JSImported (global::System.Int32 id) : global::IImported internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); global::Record? global::IImported.Record { - get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(_id); - set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(_id, value); + 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); } @@ -119,13 +117,13 @@ public partial class Class Execute(); Contains( """ - internal static int Export (global::IExported instance) => Export(instance, static (_id, instance) => { - instance.Changed += HandleChanged; + internal static int Export (global::IExported it) => Export(it, static (_id, it) => { + it.Changed += HandleChanged; return () => { - instance.Changed -= HandleChanged; + it.Changed -= HandleChanged; }; - void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.Exported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); + void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.IExported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); }); """); } @@ -135,7 +133,7 @@ public void DoesNotGenerateDuplicateSpecializedExports () { AddAssembly(With( """ - public interface IExported + public interface IBi { event Action? Changed; event Action? Done; @@ -143,10 +141,25 @@ public interface IExported public class Class { - [Export] public static IExported GetExported () => default; + [Export] public static IBi GetExported () => default!; + [Import] public static IBi GetImported () => default!; } """)); Execute(); - Once(@"internal static int Export \(global::IExported instance\)"); + 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) : Bootsharp.JSProxy(id), global::IInstanced"); } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs similarity index 58% rename from src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs index d0910c5f..1b4daf7e 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/InteropTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class InteropTest : EmitTest +public class InteropTest : GenerateCSTest { protected override string TestedContent => GeneratedInterop; @@ -29,8 +29,8 @@ public static class Registry // (the fields are emitted by the source generators) public static unsafe delegate* managed Bootsharp_GetLabel; [Import] public static string GetLabel (int count) => default!; - public static unsafe delegate* managed Bootsharp_GetPropertyCount; - public static unsafe delegate* managed Bootsharp_SetPropertyCount; + public static unsafe delegate* managed Bootsharp_GetCount; + public static unsafe delegate* managed Bootsharp_SetCount; [Import] public static int Count { get => default!; set { } } } """)); @@ -50,11 +50,11 @@ public static class App [ModuleInitializer] internal static unsafe void Initialize () { - global::Library.Registry.Evt += Handle_Library_Registry_Evt; global::Entry.App.Bootsharp_GetName = &Entry_App_GetName; + global::Library.Registry.Evt += Handle_Library_Registry_Evt; global::Library.Registry.Bootsharp_GetLabel = &Library_Registry_GetLabel; - global::Library.Registry.Bootsharp_GetPropertyCount = &Library_Registry_GetPropertyCount; - global::Library.Registry.Bootsharp_SetPropertyCount = &Library_Registry_SetPropertyCount; + global::Library.Registry.Bootsharp_GetCount = &Library_Registry_GetCount; + global::Library.Registry.Bootsharp_SetCount = &Library_Registry_SetCount; } """); } @@ -76,16 +76,16 @@ [Export] public static void Inv () {} """)); Execute(); Contains("void Handle_Class_ExpEvt () => Class_BroadcastExpEvt_Serialized();"); - Contains("""[JSImport("Class.broadcastExpEvtSerialized", "Bootsharp")] internal static partial void Class_BroadcastExpEvt_Serialized ();"""); + Contains("""[JSImport("Class.broadcastExpEvtSerialized", "index")] internal static partial void Class_BroadcastExpEvt_Serialized ();"""); Contains("[JSExport] internal static void Class_InvokeImpEvt () => global::Class.Bootsharp_Invoke_ImpEvt();"); Contains("[JSExport] internal static void Class_Inv () => global::Class.Inv();"); - Contains("""[JSImport("Class.funSerialized", "Bootsharp")] internal static partial void Class_Fun_Serialized ();"""); - Contains("[JSExport] internal static global::System.Int32 Class_GetPropertyExpProp () => global::Class.ExpProp;"); - Contains("[JSExport] internal static void Class_SetPropertyExpProp (global::System.Int32 value) => global::Class.ExpProp = value;"); - Contains("""[JSImport("Class.getPropertyImpPropSerialized", "Bootsharp")] internal static partial global::System.Int32 Class_GetPropertyImpProp_Serialized ();"""); - Contains("public static global::System.Int32 Class_GetPropertyImpProp() => Class_GetPropertyImpProp_Serialized();"); - Contains("""[JSImport("Class.setPropertyImpPropSerialized", "Bootsharp")] internal static partial void Class_SetPropertyImpProp_Serialized (global::System.Int32 value);"""); - Contains("public static void Class_SetPropertyImpProp(global::System.Int32 value) => Class_SetPropertyImpProp_Serialized(value);"); + Contains("""[JSImport("Class.funSerialized", "index")] internal static partial void Class_Fun_Serialized ();"""); + Contains("[JSExport] internal static global::System.Int32 Class_GetExpProp () => global::Class.ExpProp;"); + Contains("[JSExport] internal static void Class_SetExpProp (global::System.Int32 value) => global::Class.ExpProp = value;"); + Contains("""[JSImport("Class.getImpPropSerialized", "index")] internal static partial global::System.Int32 Class_GetImpProp_Serialized ();"""); + Contains("public static global::System.Int32 Class_GetImpProp() => Class_GetImpProp_Serialized();"); + Contains("""[JSImport("Class.setImpPropSerialized", "index")] internal static partial void Class_SetImpProp_Serialized (global::System.Int32 value);"""); + Contains("public static void Class_SetImpProp(global::System.Int32 value) => Class_SetImpProp_Serialized(value);"); } [Fact] @@ -115,19 +115,19 @@ [Export] public static void Inv () {} } """)); Execute(); - Contains("""[JSImport("SpaceA.Class.broadcastExpEvtSerialized", "Bootsharp")] internal static partial void SpaceA_Class_BroadcastExpEvt_Serialized ();"""); + Contains("""[JSImport("Class.broadcastExpEvtSerialized", "space-a")] internal static partial void SpaceA_Class_BroadcastExpEvt_Serialized ();"""); Contains("void Handle_SpaceA_Class_ExpEvt () => SpaceA_Class_BroadcastExpEvt_Serialized();"); Contains("[JSExport] internal static void SpaceA_Class_Inv () => global::SpaceA.Class.Inv();"); - Contains("""[JSImport("SpaceA.Class.funSerialized", "Bootsharp")] internal static partial void SpaceA_Class_Fun_Serialized ();"""); + Contains("""[JSImport("Class.funSerialized", "space-a")] internal static partial void SpaceA_Class_Fun_Serialized ();"""); Contains("[JSExport] internal static void SpaceA_SpaceB_Class_InvokeImpEvt () => global::SpaceA.SpaceB.Class.Bootsharp_Invoke_ImpEvt();"); Contains("[JSExport] internal static void SpaceA_SpaceB_Class_Inv () => global::SpaceA.SpaceB.Class.Inv();"); - Contains("""[JSImport("SpaceA.SpaceB.Class.funSerialized", "Bootsharp")] internal static partial void SpaceA_SpaceB_Class_Fun_Serialized ();"""); - Contains("[JSExport] internal static global::System.Int32 SpaceA_Class_GetPropertyExpProp () => global::SpaceA.Class.ExpProp;"); - Contains("[JSExport] internal static void SpaceA_Class_SetPropertyExpProp (global::System.Int32 value) => global::SpaceA.Class.ExpProp = value;"); - Contains("""[JSImport("SpaceA.SpaceB.Class.getPropertyImpPropSerialized", "Bootsharp")] internal static partial global::System.Int32 SpaceA_SpaceB_Class_GetPropertyImpProp_Serialized ();"""); - Contains("public static global::System.Int32 SpaceA_SpaceB_Class_GetPropertyImpProp() => SpaceA_SpaceB_Class_GetPropertyImpProp_Serialized();"); - Contains("""[JSImport("SpaceA.SpaceB.Class.setPropertyImpPropSerialized", "Bootsharp")] internal static partial void SpaceA_SpaceB_Class_SetPropertyImpProp_Serialized (global::System.Int32 value);"""); - Contains("public static void SpaceA_SpaceB_Class_SetPropertyImpProp(global::System.Int32 value) => SpaceA_SpaceB_Class_SetPropertyImpProp_Serialized(value);"); + Contains("""[JSImport("Class.funSerialized", "space-a/space-b")] internal static partial void SpaceA_SpaceB_Class_Fun_Serialized ();"""); + Contains("[JSExport] internal static global::System.Int32 SpaceA_Class_GetExpProp () => global::SpaceA.Class.ExpProp;"); + Contains("[JSExport] internal static void SpaceA_Class_SetExpProp (global::System.Int32 value) => global::SpaceA.Class.ExpProp = value;"); + Contains("""[JSImport("Class.getImpPropSerialized", "space-a/space-b")] internal static partial global::System.Int32 SpaceA_SpaceB_Class_GetImpProp_Serialized ();"""); + Contains("public static global::System.Int32 SpaceA_SpaceB_Class_GetImpProp() => SpaceA_SpaceB_Class_GetImpProp_Serialized();"); + Contains("""[JSImport("Class.setImpPropSerialized", "space-a/space-b")] internal static partial void SpaceA_SpaceB_Class_SetImpProp_Serialized (global::System.Int32 value);"""); + Contains("public static void SpaceA_SpaceB_Class_SetImpProp(global::System.Int32 value) => SpaceA_SpaceB_Class_SetImpProp_Serialized(value);"); } [Fact] @@ -154,8 +154,8 @@ 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 global::System.Int64 Bootsharp_Generated_Exports_Space_JSExported_Inv (global::System.String str, [JSMarshalAs] global::System.Int64 info) => Serializer.Serialize(global::Bootsharp.Generated.Exports.Space.JSExported.Inv(str, Serializer.Deserialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); - Contains("""[JSImport("Space.Imported.funSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized (global::System.String str, [JSMarshalAs] global::System.Int64 info);"""); + 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);"); } @@ -176,11 +176,11 @@ public partial class Class } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExported_Inv (global::System.Int32 _id, global::System.Int32 it, [JSMarshalAs] global::System.Int64 info) => Serializer.Serialize(Instances.Exported(_id).Inv(Instances.Exported(it), Serializer.Deserialize(info, SerializerContext.Info)), SerializerContext.Info);"); - Contains("""[JSImport("Imported.funSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImported_Fun_Serialized (global::System.Int32 _id, global::System.Int32 it, [JSMarshalAs] global::System.Int64 info);"""); - Contains("public static global::Info Bootsharp_Generated_Imports_JSImported_Fun (global::System.Int32 _id, global::IImported it, global::Info info) => Serializer.Deserialize(Bootsharp_Generated_Imports_JSImported_Fun_Serialized(_id, ((global::Bootsharp.Generated.Imports.JSImported)it)._id, Serializer.Serialize(info, SerializerContext.Info)), SerializerContext.Info);"); - Contains("[JSExport] internal static async global::System.Threading.Tasks.Task Class_GetExported (global::System.Int32 it) => Instances.Export(await global::Class.GetExported(Instances.Import(it, static id => new global::Bootsharp.Generated.Imports.JSImported(id))));"); - Contains("""[JSImport("Class.getImportedSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Class_GetImported_Serialized (global::System.Int32 it);"""); + 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] 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);"""); } [Fact] @@ -210,18 +210,18 @@ public interface IImported } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_Space_JSExported_GetPropertyState () => Serializer.Serialize(global::Bootsharp.Generated.Exports.Space.JSExported.GetPropertyState(), SerializerContext.Space_Info);"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_SetPropertyState ([JSMarshalAs] global::System.Int64 value) => global::Bootsharp.Generated.Exports.Space.JSExported.SetPropertyState(Serializer.Deserialize(value, SerializerContext.Space_Info));"); - Contains("""[JSImport("Space.Imported.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Space_Imported_GetPropertyState_Serialized ();"""); - Contains("public static global::Space.Info Bootsharp_Generated_Imports_Space_JSImported_GetPropertyState() => Serializer.Deserialize(Space_Imported_GetPropertyState_Serialized(), SerializerContext.Space_Info);"); - Contains("""[JSImport("Space.Imported.setPropertyStateSerialized", "Bootsharp")] internal static partial void Space_Imported_SetPropertyState_Serialized ([JSMarshalAs] global::System.Int64 value);"""); - Contains("public static void Bootsharp_Generated_Imports_Space_JSImported_SetPropertyState(global::Space.Info value) => Space_Imported_SetPropertyState_Serialized(Serializer.Serialize(value, SerializerContext.Space_Info));"); - Contains("[JSExport] internal static global::System.Boolean Bootsharp_Generated_Exports_Space_JSExported_GetPropertyActive () => global::Bootsharp.Generated.Exports.Space.JSExported.GetPropertyActive();"); - Contains("""[JSImport("Space.Imported.getPropertyActiveSerialized", "Bootsharp")] internal static partial global::System.Boolean Space_Imported_GetPropertyActive_Serialized ();"""); - Contains("public static global::System.Boolean Bootsharp_Generated_Imports_Space_JSImported_GetPropertyActive() => Space_Imported_GetPropertyActive_Serialized();"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_SetPropertyCount (global::System.Int32 value) => global::Bootsharp.Generated.Exports.Space.JSExported.SetPropertyCount(value);"); - Contains("""[JSImport("Space.Imported.setPropertyCountSerialized", "Bootsharp")] internal static partial void Space_Imported_SetPropertyCount_Serialized (global::System.Int32 value);"""); - Contains("public static void Bootsharp_Generated_Imports_Space_JSImported_SetPropertyCount(global::System.Int32 value) => Space_Imported_SetPropertyCount_Serialized(value);"); + 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("""[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("""[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("""[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("""[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);"); } [Fact] @@ -252,15 +252,15 @@ public class Class } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExported_GetPropertyState (global::System.Int32 _id) => Serializer.Serialize(Instances.Exported(_id).State, SerializerContext.Info);"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_JSExported_SetPropertyState (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 value) => Instances.Exported(_id).State = Serializer.Deserialize(value, SerializerContext.Info);"); - Contains("""[JSImport("Imported.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Imported_GetPropertyState_Serialized (global::System.Int32 _id);"""); - Contains("[JSExport] internal static global::System.Int32 Bootsharp_Generated_Exports_JSExported_GetPropertyExported (global::System.Int32 _id) => Instances.Export(Instances.Exported(_id).Exported);"); - Contains("[JSExport] internal static void Bootsharp_Generated_Exports_JSExported_SetPropertyImported (global::System.Int32 _id, global::System.Int32 value) => Instances.Exported(_id).Imported = Instances.Import(value, static id => new global::Bootsharp.Generated.Imports.JSImported(id));"); - Contains("""[JSImport("Imported.getPropertyImportedSerialized", "Bootsharp")] internal static partial global::System.Int32 Imported_GetPropertyImported_Serialized (global::System.Int32 _id);"""); - Contains("public static global::IImported Bootsharp_Generated_Imports_JSImported_GetPropertyImported(global::System.Int32 _id) => Instances.Import(Imported_GetPropertyImported_Serialized(_id), static id => new global::Bootsharp.Generated.Imports.JSImported(id));"); - Contains("""[JSImport("Imported.setPropertyExportedSerialized", "Bootsharp")] internal static partial void Imported_SetPropertyExported_Serialized (global::System.Int32 _id, global::System.Int32 value);"""); - Contains("public static void Bootsharp_Generated_Imports_JSImported_SetPropertyExported(global::System.Int32 _id, global::IExported value) => Imported_SetPropertyExported_Serialized(_id, Instances.Export(value));"); + 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("""[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("""[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("""[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));"); } [Fact] @@ -287,9 +287,9 @@ internal static unsafe void Initialize () global::Bootsharp.Generated.Exports.Space.JSExported.Evt += Handle_Bootsharp_Generated_Exports_Space_JSExported_Evt; } """); - Contains("void Handle_Bootsharp_Generated_Exports_Space_JSExported_Evt (global::Space.Info obj) => Space_Exported_BroadcastEvt_Serialized(Serializer.Serialize(obj, SerializerContext.Space_Info));"); - Contains("""[JSImport("Space.Exported.broadcastEvtSerialized", "Bootsharp")] internal static partial void Space_Exported_BroadcastEvt_Serialized ([JSMarshalAs] global::System.Int64 obj);"""); - Contains("[JSExport] internal static void Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt ([JSMarshalAs] global::System.Int64 obj) => ((global::Bootsharp.Generated.Imports.Space.JSImported)Modules.Imports[typeof(global::Space.IImported)].Instance).InvokeEvt(Serializer.Deserialize(obj, SerializerContext.Space_Info));"); + Contains("void Handle_Bootsharp_Generated_Exports_Space_JSExported_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));"); } [Fact] @@ -309,8 +309,24 @@ public partial class Class } """)); Execute(); - Contains("""[JSImport("Exported.broadcastChangedSerialized", "Bootsharp")] internal static partial void Exported_BroadcastChanged_Serialized (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 arg1, global::System.Int32 arg2);"""); - Contains("[JSExport] internal static void Bootsharp_Generated_Imports_JSImported_InvokeChanged (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 arg1, global::System.Int32 arg2) => Instances.Import(_id, static id => new global::Bootsharp.Generated.Imports.JSImported(id)).InvokeChanged(Serializer.Deserialize(arg1, SerializerContext.Record), Instances.Import(arg2, static id => new global::Bootsharp.Generated.Imports.JSImported(id)));"); + 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));"); + } + + [Fact] + public void DoesNotEmitStaticEventSubscriptionForInstanceEvents () + { + AddAssembly(With( + """ + public interface IExportedInstance { event Action? OnChanged; } + + public class Class + { + [Export] public static IExportedInstance Get () => default!; + } + """)); + Execute(); + DoesNotContain("OnChanged +="); } [Fact] @@ -345,8 +361,8 @@ public class Class DoesNotContain("DefaultGet"); DoesNotContain("DefaultSet"); DoesNotContain("BothDefault"); - DoesNotContain("GetPropertyItem"); - DoesNotContain("SetPropertyItem"); + DoesNotContain("GetItem"); + DoesNotContain("SetItem"); } [Fact] @@ -373,7 +389,7 @@ public class Class } [Fact] - public void DoesntSerializeTypesThatShouldNotBeSerialized () + public void DoesNotSerializeTypesThatShouldNotBeSerialized () { AddAssembly(With( """ @@ -390,9 +406,9 @@ public class Class Execute(); Contains("[JSExport] internal static global::System.Threading.Tasks.Task Space_Class_Inv (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12) => global::Space.Class.Inv(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); Contains("[JSExport] [return: JSMarshalAs>] internal static global::System.Threading.Tasks.Task Space_Class_InvNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12) => global::Space.Class.InvNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); - Contains("""[JSImport("Space.Class.funSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_Fun_Serialized (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12);"""); + Contains("""[JSImport("Class.funSerialized", "space")] internal static partial global::System.Threading.Tasks.Task Space_Class_Fun_Serialized (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12);"""); Contains("public static global::System.Threading.Tasks.Task Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull_Serialized(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); - Contains("""[JSImport("Space.Class.funNullSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunNull_Serialized (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12);"""); + Contains("""[JSImport("Class.funNullSerialized", "space")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunNull_Serialized (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12);"""); Contains("public static global::System.Threading.Tasks.Task Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull_Serialized(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); } @@ -416,14 +432,14 @@ public partial class Class } """)); Execute(); - Contains("""[JSImport("Space.Class.broadcastExpEvtSerialized", "Bootsharp")] internal static partial void Space_Class_BroadcastExpEvt_Serialized ([JSMarshalAs] global::System.Int64 obj);"""); + Contains("""[JSImport("Class.broadcastExpEvtSerialized", "space")] internal static partial void Space_Class_BroadcastExpEvt_Serialized ([JSMarshalAs] long obj);"""); Contains("void Handle_Space_Class_ExpEvt (global::Space.Record obj) => Space_Class_BroadcastExpEvt_Serialized(Serializer.Serialize(obj, SerializerContext.Space_Record));"); - Contains("[JSExport] internal static void Space_Class_InvokeImpEvt ([JSMarshalAs] global::System.Int64 arg1, global::System.Int32 arg2) => global::Space.Class.Bootsharp_Invoke_ImpEvt(Serializer.Deserialize(arg1, SerializerContext.Space_Record), arg2);"); - Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Space_Class_InvA ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(global::Space.Class.InvA(Serializer.Deserialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); - Contains("[JSExport] [return: JSMarshalAs>] internal static async global::System.Threading.Tasks.Task Space_Class_InvB ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(await global::Space.Class.InvB(Serializer.Deserialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); - Contains("""[JSImport("Space.Class.funASerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Space_Class_FunA_Serialized ([JSMarshalAs] global::System.Int64 a);"""); + Contains("[JSExport] internal static void Space_Class_InvokeImpEvt ([JSMarshalAs] long arg1, global::System.Int32 arg2) => global::Space.Class.Bootsharp_Invoke_ImpEvt(Serializer.Deserialize(arg1, SerializerContext.Space_Record), arg2);"); + Contains("[JSExport] [return: JSMarshalAs] internal static long Space_Class_InvA ([JSMarshalAs] long a) => Serializer.Serialize(global::Space.Class.InvA(Serializer.Deserialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); + Contains("[JSExport] [return: JSMarshalAs>] internal static async global::System.Threading.Tasks.Task Space_Class_InvB ([JSMarshalAs] long a) => Serializer.Serialize(await global::Space.Class.InvB(Serializer.Deserialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); + Contains("""[JSImport("Class.funASerialized", "space")] [return: JSMarshalAs] internal static partial long Space_Class_FunA_Serialized ([JSMarshalAs] long a);"""); Contains("public static global::Space.Record Space_Class_FunA (global::Space.Record a) => Serializer.Deserialize(Space_Class_FunA_Serialized(Serializer.Serialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); - Contains("""[JSImport("Space.Class.funBSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB_Serialized ([JSMarshalAs] global::System.Int64 a);"""); + Contains("""[JSImport("Class.funBSerialized", "space")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB_Serialized ([JSMarshalAs] long a);"""); 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);"); } @@ -450,10 +466,10 @@ [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("Foo.Imported.funSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized ();"""); - Contains("""[JSImport("Foo.Class.broadcastEvtSerialized", "Bootsharp")] internal static partial void Foo_Class_BroadcastEvt_Serialized ();"""); - Contains("void Handle_Space_Class_Evt () => Foo_Class_BroadcastEvt_Serialized();"); + Contains("""[JSImport("IImported.funSerialized", "foo")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_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();"); - Contains("""[JSImport("Foo.Class.funSerialized", "Bootsharp")] internal static partial void Space_Class_Fun_Serialized ();"""); + Contains("""[JSImport("Class.funSerialized", "foo")] internal static partial void Space_Class_Fun_Serialized ();"""); } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs similarity index 89% rename from src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs index bc591a55..e055af97 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/ModulesTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class ModulesTest : EmitTest +public class ModulesTest : GenerateCSTest { protected override string TestedContent => GeneratedModules; @@ -39,7 +39,7 @@ internal static class ModuleRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterModules () { - Modules.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::IExported), handler => new Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); + Modules.Register(typeof(global::Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::IExported), handler => new global::Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); } } } @@ -59,8 +59,8 @@ public JSExported (global::IExported handler) [Export] public static event global::System.Action OnRecordChanged; [Export] public static event global::IExported.SomethingChanged OnSomethingChanged; - [Export] public static global::Record? GetPropertyRecord () => handler.Record; - [Export] public static void SetPropertyRecord (global::Record? value) => handler.Record = value; + [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(); @@ -106,7 +106,7 @@ internal static class ModuleRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterModules () { - Modules.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::Exported), handler => new Bootsharp.Generated.Exports.JSExported((global::Exported)handler))); + Modules.Register(typeof(global::Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::Exported), handler => new global::Bootsharp.Generated.Exports.JSExported((global::Exported)handler))); } } } @@ -126,8 +126,8 @@ public JSExported (global::Exported handler) [Export] public static event global::System.Action OnRecordChanged; [Export] public static event global::Exported.SomethingChanged OnSomethingChanged; - [Export] public static global::Record? GetPropertyRecord () => handler.Record; - [Export] public static void SetPropertyRecord (global::Record? value) => handler.Record = value; + [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(); @@ -189,7 +189,7 @@ internal static class ModuleRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterModules () { - Modules.Register(typeof(global::IImported), new ImportModule(new Bootsharp.Generated.Imports.JSImported())); + Modules.Register(typeof(global::IImported), new ImportModule(new global::Bootsharp.Generated.Imports.JSImported())); } } } @@ -204,8 +204,8 @@ public class JSImported : global::IImported internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); global::Record? global::IImported.Record { - get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(); - set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(value); + 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(); @@ -258,8 +258,8 @@ internal static class ModuleRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterModules () { - Modules.Register(typeof(Bootsharp.Generated.Exports.Space.JSExported), new ExportModule(typeof(global::Space.IExported), handler => new Bootsharp.Generated.Exports.Space.JSExported((global::Space.IExported)handler))); - Modules.Register(typeof(global::Space.IImported), new ImportModule(new Bootsharp.Generated.Imports.Space.JSImported())); + 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())); } } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/SerializerTest.cs similarity index 99% rename from src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateCS/SerializerTest.cs index f81d0f96..8fc3f960 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/SerializerTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class SerializerTest : EmitTest +public class SerializerTest : GenerateCSTest { protected override string TestedContent => GeneratedSerializer; diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs similarity index 71% rename from src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index 60a7d80d..b214ccf3 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -1,17 +1,17 @@ namespace Bootsharp.Publish.Test; -public class DeclarationTest : PackTest +public class DeclarationTest : GenerateJSTest { - protected override string TestedContent => GeneratedDeclarations; + protected override string TestedContent { get => field ?? ReadProjectFile("generated/index.g.d.mts") ?? ""; set; } [Fact] public void DeclaresNamespace () { AddAssembly(WithClass("Foo", "[Export] public static void Bar () { }")); Execute(); - Contains( + Contains("foo.g.d.mts", """ - export namespace Foo.Class { + export namespace Class { export function bar(): void; } """); @@ -22,9 +22,9 @@ public void DotsInSpaceArePreserved () { AddAssembly(WithClass("Foo.Bar.Nya", "[Export] public static void Bar () { }")); Execute(); - Contains( + Contains("foo/bar/nya.g.d.mts", """ - export namespace Foo.Bar.Nya.Class { + export namespace Class { export function bar(): void; } """); @@ -40,16 +40,15 @@ public void WhenNoNamespaceDeclaresUnderRoot () Execute(); Contains( """ + export namespace Class { + export function inv(r: Record): Enum; + } export enum Enum { A, B } export type Record = Readonly<{ }>; - - export namespace Class { - export function inv(r: Record): Enum; - } """); } @@ -62,15 +61,57 @@ public void NestedTypesAreDeclaredUnderClassSpace () Execute(); Contains( """ + export namespace Class { + export function inv(r: Foo.Bar): void; + } export namespace Foo { export type Bar = Readonly<{ }>; } + """); + } - export namespace Class { - export function inv(r: Foo.Bar): void; + [Fact] + public void CrawledTypeDoesNotOverrideSpecializedDeclaration () + { + AddAssembly(With( + """ + [assembly:Export(typeof(ModFoo))] + + public class StatFoo + { + [Export] public static int StatBar () => 0; + public record StatItem (int X); } - """); + public class InstFoo + { + public int Value { get; set; } + public record InstItem (int X); + } + public record SerFoo (int X) + { + public record SerItem (int Y); + } + public interface ModFoo + { + int ModBar (); + public record ModItem (int X); + } + public class Class + { + [Export] public static int UseStat (StatFoo.StatItem i) => 0; + [Export] public static InstFoo GetInst () => default; + [Export] public static int UseInst (InstFoo.InstItem i) => 0; + [Export] public static SerFoo GetSer () => default; + [Export] public static int UseSer (SerFoo.SerItem i) => 0; + [Export] public static int UseMod (ModFoo.ModItem i) => 0; + } + """)); + Execute(); + Contains("export function statBar(): number;"); + Contains("export interface InstFoo"); + Contains("export type SerFoo"); + Contains("export function modBar(): number;"); } [Fact] @@ -78,9 +119,9 @@ public void FunctionDeclarationIsExportedForInvokableMethod () { AddAssembly(WithClass("Foo", "[Export] public static void Foo () { }")); Execute(); - Contains( + Contains("foo.g.d.mts", """ - export namespace Foo.Class { + export namespace Class { export function foo(): void; } """); @@ -91,9 +132,9 @@ public void AssignableVariableIsExportedForFunctionCallback () { AddAssembly(WithClass("Foo", "[Import] public static void OnFoo () { }")); Execute(); - Contains( + Contains("foo.g.d.mts", """ - export namespace Foo.Class { + export namespace Class { export let onFoo: () => void; } """); @@ -107,12 +148,12 @@ public void EventPropertiesAreExportedForStaticEvents () WithClass("Foo", "[Export] public static event Action? Evt;"), WithClass("Foo", "[Import] public static event Action? ImpEvt;")); Execute(); - Contains( + Contains("foo.g.d.mts", """ - export namespace Foo.Class { - export const expEvt: EventSubscriber<[]>; - export const evt: EventSubscriber<[obj: string]>; - export const impEvt: EventBroadcaster<[arg1: number, arg2: boolean | undefined]>; + export namespace Class { + export const expEvt: Event<[]>; + export const evt: Event<[obj: string]>; + export const impEvt: Event<[arg1: number, arg2: boolean | undefined]>; } """); } @@ -125,9 +166,9 @@ public void VariablesAreExportedForStaticProperties () WithClass("Foo", "[Export] public static string ReadOnlyProp { get; }"), WithClass("Foo", "[Import] public static bool? ImpProp { get => default; set { } }")); Execute(); - Contains( + Contains("foo.g.d.mts", """ - export namespace Foo.Class { + export namespace Class { export let expProp: number; export const readOnlyProp: string; export let impProp: { get: () => boolean | undefined; set: (value: boolean | undefined) => void }; @@ -143,17 +184,14 @@ public void MembersFromSameSpaceAreDeclaredUnderSameSpace () With("Space", "public class Bar { }"), WithClass("Space", "[Export] public static Foo GetFoo (Bar bar) => default;")); Execute(); - Contains( + Contains("space.g.d.mts", """ - export namespace Space { - export interface Bar { - } - export interface Foo { - } + export namespace Class { + export function getFoo(bar: Bar): Foo; } - - export namespace Space.Class { - export function getFoo(bar: Space.Bar): Space.Foo; + export interface Bar { + } + export interface Foo { } """); } @@ -168,17 +206,8 @@ public void MembersFromDifferentSpacesAreDeclaredUnderRespectiveSpaces () Execute(); Contains( """ - export namespace SpaceA { - export interface Foo { - } - } - export namespace SpaceB { - export interface Bar { - } - } - export namespace Class { - export function getFoo(bar: SpaceB.Bar): SpaceA.Foo; + export function getFoo(bar: space_b.Bar): space_a.Foo; } """); } @@ -190,12 +219,15 @@ public void DifferentSpacesWithSameRootAreDeclaredIndividually () WithClass("Nya.Bar", "[Export] public static void Fun () { }"), WithClass("Nya.Foo", "[Export] public static void Foo () { }")); Execute(); - Contains( + Contains("nya/bar.g.d.mts", """ - export namespace Nya.Bar.Class { + export namespace Class { export function fun(): void; } - export namespace Nya.Foo.Class { + """); + Contains("nya/foo.g.d.mts", + """ + export namespace Class { export function foo(): void; } """); @@ -210,12 +242,11 @@ public void WhenNoNamespaceTypesAreDeclaredUnderRoot () Execute(); Contains( """ - export interface Foo { - } - export namespace Class { export let onFoo: (foo: Foo) => void; } + export interface Foo { + } """); } @@ -303,6 +334,7 @@ public void IntArraysTranslatedToRelatedTypes () WithClass("[Export] public static void Float32 (float[] foo) {}"), WithClass("[Export] public static void Float64 (double[] foo) {}")); Execute(); + TestedContent = ReadProjectFile("generated/index.g.d.mts"); Contains("uint8(foo: Uint8Array): void"); Contains("int8(foo: Int8Array): void"); Contains("uint16(foo: Uint16Array): void"); @@ -329,17 +361,14 @@ public void DefinitionIsGeneratedForObjectType () 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;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Foo { - s: string; - i: number; - } + export namespace Class { + export function method(t: Foo): Foo; } - - export namespace n.Class { - export function method(t: n.Foo): n.Foo; + export interface Foo { + s: string; + i: number; } """); } @@ -353,23 +382,20 @@ public void DefinitionIsGeneratedForInterfaceAndImplementation () 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;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Base { - } - export interface Derived extends n.Base, n.Interface { - readonly foo: n.Interface; - bar(b: n.Interface): void; - } - export interface Interface { - readonly foo: n.Interface; - bar(b: n.Interface): void; - } + export namespace Class { + export function method(b: Base): Derived; } - - export namespace n.Class { - export function method(b: n.Base): n.Derived; + export interface Base { + } + export interface Derived extends Base, Interface { + readonly foo: Interface; + bar(b: Interface): void; + } + export interface Interface { + readonly foo: Interface; + bar(b: Interface): void; } """); } @@ -382,18 +408,15 @@ public void DefinitionIsGeneratedForTypeWithListProperty () With("n", "public class Container { public List Items { get; } }"), WithClass("n", "[Export] public static Container Combine (List items) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Item { - } - export interface Container { - readonly items: Array; - } + export namespace Class { + export function combine(items: Array): Container; } - - export namespace n.Class { - export function combine(items: Array): n.Container; + export interface Item { + } + export interface Container { + readonly items: Array; } """); } @@ -406,18 +429,15 @@ public void DefinitionIsGeneratedForTypeWithJaggedArrayProperty () With("n", "public class Container { public Item[][] Items { get; } }"), WithClass("n", "[Export] public static Container Get () => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Container { - readonly items: Array>; - } - export interface Item { - } + export namespace Class { + export function get(): Container; } - - export namespace n.Class { - export function get(): n.Container; + export interface Container { + readonly items: Array>; + } + export interface Item { } """); } @@ -430,18 +450,15 @@ public void DefinitionIsGeneratedForTypeWithReadOnlyListProperty () With("n", "public class Container { public IReadOnlyList Items { get; } }"), WithClass("n", "[Export] public static Container Combine (IReadOnlyList items) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Item { - } - export interface Container { - readonly items: Array; - } + export namespace Class { + export function combine(items: Array): Container; } - - export namespace n.Class { - export function combine(items: Array): n.Container; + export interface Item { + } + export interface Container { + readonly items: Array; } """); } @@ -454,18 +471,15 @@ public void DefinitionIsGeneratedForTypeWithDictionaryProperty () With("n", "public class Container { public Dictionary Items { get; } }"), WithClass("n", "[Export] public static Container Combine (Dictionary items) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Item { - } - export interface Container { - readonly items: Map; - } + export namespace Class { + export function combine(items: Map): Container; } - - export namespace n.Class { - export function combine(items: Map): n.Container; + export interface Item { + } + export interface Container { + readonly items: Map; } """); } @@ -478,18 +492,15 @@ public void DefinitionIsGeneratedForTypeWithReadOnlyDictionaryProperty () With("n", "public class Container { public IReadOnlyDictionary Items { get; } }"), WithClass("n", "[Export] public static Container Combine (IReadOnlyDictionary items) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Item { - } - export interface Container { - readonly items: Map; - } + export namespace Class { + export function combine(items: Map): Container; } - - export namespace n.Class { - export function combine(items: Map): n.Container; + export interface Item { + } + export interface Container { + readonly items: Map; } """); } @@ -502,18 +513,15 @@ public void DefinitionIsGeneratedForTypeWithCollectionProperty () With("n", "public class Container { public ICollection Items { get; } }"), WithClass("n", "[Export] public static Container Combine (ICollection items) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Item { - } - export interface Container { - readonly items: Array; - } + export namespace Class { + export function combine(items: Array): Container; } - - export namespace n.Class { - export function combine(items: Array): n.Container; + export interface Item { + } + export interface Container { + readonly items: Array; } """); } @@ -526,18 +534,15 @@ public void DefinitionIsGeneratedForTypeWithReadOnlyCollectionProperty () With("n", "public class Container { public IReadOnlyCollection Items { get; } }"), WithClass("n", "[Export] public static Container Combine (IReadOnlyCollection items) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Item { - } - export interface Container { - readonly items: Array; - } + export namespace Class { + export function combine(items: Array): Container; } - - export namespace n.Class { - export function combine(items: Array): n.Container; + export interface Item { + } + export interface Container { + readonly items: Array; } """); } @@ -550,20 +555,17 @@ public void DefinitionIsGeneratedForGenericClass () 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) { }")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Generic { - value: T; - } - export interface GenericNull { - readonly value?: T; - foo(t: T | undefined): T | null; - } + export namespace Class { + export function method(a: Generic, b: GenericNull): void; } - - export namespace n.Class { - export function method(a: n.Generic, b: n.GenericNull): void; + export interface Generic { + value: T; + } + export interface GenericNull { + readonly value?: T; + foo(t: T | undefined): T | null; } """); } @@ -576,20 +578,17 @@ public void DefinitionIsGeneratedForGenericRecord () With("n", "public record GenericNull { public T? Value { get; set; } }"), WithClass("n", "[Export] public static void Method (Generic a, GenericNull b) { }")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export type Generic = Readonly<{ - value: T; - }>; - export type GenericNull = Readonly<{ - value?: T; - }>; - } - - export namespace n.Class { - export function method(a: n.Generic, b: n.GenericNull): void; + export namespace Class { + export function method(a: Generic, b: GenericNull): void; } + export type Generic = Readonly<{ + value: T; + }>; + export type GenericNull = Readonly<{ + value?: T; + }>; """); } @@ -600,16 +599,13 @@ public void DefinitionIsGeneratedForGenericInterface () With("n", "public interface IGenericInterface { public T Value { get; set; } }"), WithClass("n", "[Export] public static IGenericInterface Method () => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface IGenericInterface { - value?: T; - } + export namespace Class { + export function method(): IGenericInterface; } - - export namespace n.Class { - export function method(): n.IGenericInterface; + export interface IGenericInterface { + value?: T; } """); } @@ -622,21 +618,10 @@ public void DefinitionIsGeneratedForNestedGenericTypes () With("Bar", "public interface GenericInterface { public T Value { get; set; } }"), WithClass("n", "[Export] public static void Method (Foo.GenericClass> p) { }")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace Bar { - export interface GenericInterface { - value?: T; - } - } - export namespace Foo { - export interface GenericClass { - value?: T; - } - } - - export namespace n.Class { - export function method(p: Foo.GenericClass>): void; + export namespace Class { + export function method(p: foo.GenericClass>): void; } """); } @@ -648,17 +633,14 @@ public void DefinitionIsGeneratedForGenericClassWithMultipleTypeArguments () With("n", "public class GenericClass { public T1 Key { get; set; } public T2 Value { get; set; } }"), WithClass("n", "[Export] public static void Method (GenericClass p) { }")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface GenericClass { - key?: T1; - value?: T2; - } + export namespace Class { + export function method(p: GenericClass): void; } - - export namespace n.Class { - export function method(p: n.GenericClass): void; + export interface GenericClass { + key?: T1; + value?: T2; } """); } @@ -686,49 +668,46 @@ public class Class { [Export] public static Dictionary GetBaz () => de Execute(); // 'Foo' and 'RecordClass' are not declared, because they don't directly appear on the interop boundary; // instead, their members are merged into 'Bar' and 'RecordClassA', who directly inherit (extend) them. - Contains( + Contains("space.g.d.mts", """ - export namespace Space { - export interface Key extends Space.Baz { - } - export interface Bar { - readonly rc: Map; - readonly s: Space.Struct; - readonly rs: Space.ReadonlyStruct; - } - export interface Nya { - mew(): boolean; - } - export interface Baz extends Space.Bar { - readonly bars: Array; - readonly e: Space.Enum; - } - export enum Enum { - A, - B - } - export type ReadonlyRecordStruct = Readonly<{ - a: number; - }>; - export type ReadonlyStruct = Readonly<{ - a: number; - }>; - export type RecordClassA = Readonly<{ - a: number; - str: Space.ReadonlyRecordStruct; - }>; - export type RecordClassB = Space.RecordClassA & Readonly<{ - b: Space.RecordClassA; - }>; - export type Struct = Readonly<{ - a: number; - mew: Space.Nya; - }>; + export namespace Class { + export function getBaz(): Map; } - - export namespace Space.Class { - export function getBaz(): Map; + export interface Key extends Baz { + } + export interface Bar { + readonly rc: Map; + readonly s: Struct; + readonly rs: ReadonlyStruct; + } + export interface Nya { + mew(): boolean; + } + export interface Baz extends Bar { + readonly bars: Array; + readonly e: Enum; + } + export enum Enum { + A, + B } + export type ReadonlyRecordStruct = Readonly<{ + a: number; + }>; + export type ReadonlyStruct = Readonly<{ + a: number; + }>; + export type RecordClassA = Readonly<{ + a: number; + str: ReadonlyRecordStruct; + }>; + export type RecordClassB = RecordClassA & Readonly<{ + b: RecordClassA; + }>; + export type Struct = Readonly<{ + a: number; + mew: Nya; + }>; """); } @@ -742,6 +721,7 @@ public void StaticPropertiesAreIncluded () Contains( """ export namespace Class { + export function bar(): Class.Foo; export interface Foo { readonly soo: string; } @@ -765,6 +745,7 @@ public record Foo Contains( """ export namespace Class { + export function bar(): Class.Foo; export type Foo = Readonly<{ }>; } @@ -787,6 +768,7 @@ public bool SetOnly { set { } } Contains( """ export namespace Class { + export function bar(): Class.Foo; export type Foo = Readonly<{ }>; } @@ -809,6 +791,7 @@ public record Foo Contains( """ export namespace Class { + export function bar(): Class.Foo; export type Foo = Readonly<{ }>; } @@ -831,16 +814,15 @@ public interface IImported { Info Fun (string str, Info info); } Execute(); Contains( """ - export type Info = Readonly<{ - value: string; - }>; - - export namespace Exported { + export namespace IExported { export function inv(str: string, info: Info): Info; } - export namespace Imported { + export namespace IImported { export let fun: (str: string, info: Info) => Info; } + export type Info = Readonly<{ + value: string; + }>; """); } @@ -863,6 +845,10 @@ public class Class Execute(); Contains( """ + export namespace Class { + export function getExported(it: IImported): Promise; + export let getImported: (it: IExported) => Promise; + } export interface IImported { fun(info: Info, str: string): Info; } @@ -873,11 +859,6 @@ export interface IExported { export type Info = Readonly<{ value: string; }>; - - export namespace Class { - export function getExported(it: IImported): Promise; - export let getImported: (it: IExported) => Promise; - } """); } @@ -912,25 +893,24 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export interface IExportedInstanced { - } - export interface IImportedInstanced { - } - export type Info = Readonly<{ - value: string; - }>; - - export namespace ExportedStatic { + export namespace IExportedStatic { export let state: Info; export const optional: Info | undefined; export const exported: IExportedInstanced; export let imported: IImportedInstanced; } - export namespace ImportedStatic { + export namespace IImportedStatic { export let state: { get: () => Info }; export let imported: { get: () => IImportedInstanced }; export let exported: { set: (value: IExportedInstanced) => void }; } + export interface IExportedInstanced { + } + export interface IImportedInstanced { + } + export type Info = Readonly<{ + value: string; + }>; """); } @@ -964,6 +944,10 @@ public class Class Execute(); Contains( """ + export namespace Class { + export function getExported(it: IImported): IExported; + export let getImported: (it: IExported) => IImported; + } export interface IImported { state: Info; readonly imported: IImported; @@ -977,11 +961,6 @@ export interface IExported { export type Info = Readonly<{ value: string; }>; - - export namespace Class { - export function getExported(it: IImported): IExported; - export let getImported: (it: IExported) => IImported; - } """); } @@ -1004,6 +983,12 @@ public interface IImportedInstanced {} Execute(); Contains( """ + export namespace IExported { + export const evt: Event<[arg1: string, arg2: Info, arg3: IExportedInstanced]>; + } + export namespace IImported { + export const evt: Event<[arg1: string, arg2: Info, arg3: IImportedInstanced]>; + } export interface IExportedInstanced { } export interface IImportedInstanced { @@ -1011,13 +996,6 @@ export interface IImportedInstanced { export type Info = Readonly<{ value: string; }>; - - export namespace Exported { - export const evt: EventSubscriber<[arg1: string, arg2: Info, arg3: IExportedInstanced]>; - } - export namespace Imported { - export const evt: EventBroadcaster<[arg1: string, arg2: Info, arg3: IImportedInstanced]>; - } """); } @@ -1040,21 +1018,20 @@ public class Class Execute(); Contains( """ + export namespace Class { + export function getExported(it: IImported): IExported; + export let getImported: (it: IExported) => IImported; + } export interface IImported { - changed: EventBroadcaster<[arg1: IImported, arg2: Info, arg3: string]>; + changed: Event<[arg1: IImported, arg2: Info, arg3: string]>; } export interface IExported { - changed: EventSubscriber<[obj: Info]>; - done: EventSubscriber<[]>; + changed: Event<[obj: Info]>; + done: Event<[]>; } export type Info = Readonly<{ value: string; }>; - - export namespace Class { - export function getExported(it: IImported): IExported; - export let getImported: (it: IExported) => IImported; - } """); } @@ -1079,6 +1056,7 @@ public void NullableMethodReturnTypesUnionWithNull () WithClass("[Export] public static Task? Quz () => default;"), WithClass("[Import] public static ValueTask?> Nya () => default;")); Execute(); + TestedContent = ReadProjectFile("generated/index.g.d.mts"); Contains("export function foo(): string | null;"); Contains("export function bar(): Promise | null;"); Contains("export function baz(): Promise;"); @@ -1095,12 +1073,11 @@ public void NullableCollectionElementTypesUnionWithNull () Execute(); Contains( """ - export interface Foo { - } - export namespace Class { export let fun: (bar: Array | undefined, nya: Array | null> | undefined, far: Array | null> | undefined) => Array | null; } + export interface Foo { + } """); } @@ -1133,19 +1110,16 @@ public void NullablePropertiesHaveOptionalModificator () With("n", "public class Bar { public Foo? Foo { get; } }"), WithClass("n", "[Export] public static Foo FooBar (Bar bar) => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Bar { - readonly foo?: n.Foo; - } - export interface Foo { - readonly bool?: boolean; - } + export namespace Class { + export function fooBar(bar: Bar): Foo; } - - export namespace n.Class { - export function fooBar(bar: n.Bar): n.Foo; + export interface Bar { + readonly foo?: Foo; + } + export interface Foo { + readonly bool?: boolean; } """); } @@ -1158,20 +1132,48 @@ public void NullableEnumsAreCrawled () With("n", "public class Bar { public Foo? Foo { get; } }"), WithClass("n", "[Export] public static Bar GetBar () => default;")); Execute(); - Contains( + Contains("n.g.d.mts", """ - export namespace n { - export interface Bar { - readonly foo?: n.Foo; - } - export enum Foo { - A, - B - } + export namespace Class { + export function getBar(): Bar; } + export interface Bar { + readonly foo?: Foo; + } + export enum Foo { + A, + B + } + """); + } - export namespace n.Class { - export function getBar(): n.Bar; + [Fact] + public void DeeplyNestedEnumIsDeclared () + { + AddAssembly(With( + """ + public class A + { + public class B + { + public enum C { X, Y } + } + } + public class Class + { + [Export] public static A.B.C Get () => default; + } + """)); + Execute(); + Contains( + """ + export namespace A { + export namespace B { + export enum C { + X, + Y + } + } } """); } @@ -1187,137 +1189,232 @@ public void WhenTypeReferencedMultipleTimesItsDeclaredOnlyOnce () WithClass("[Export] public static Foo TakeBarGiveFoo (Bar b) => default;"), WithClass("[Export] public static Far TakeAllGiveFar (Foo f, Bar b, Far ff) => default;")); Execute(); + TestedContent = ReadProjectFile("generated/index.g.d.mts"); Once("export interface Foo"); Once("export interface Bar"); Once("export interface Far"); } [Fact] - public void RespectsSpacePrefInStaticMembers () + public void IgnoresBindingsInGeneratedNamespace () { - AddAssembly( - With( - """ - [assembly: Bootsharp.Preferences( - Space = [@"^Foo\.Bar\.(\S+)", "$1"] - )] - """), - With("Foo.Bar.Nya", "public class Nya { }"), - WithClass("Foo.Bar.Fun", "[Import] public static void OnFun (Nya.Nya nya) { }")); + AddAssembly(With("Bootsharp.Generated", + """ + public record Record; + public static class Exports { [Export] public static void Inv (Record r) {} } + public static class Imports { [Import] public static void Fun () {} } + """)); Execute(); - Contains( + DoesNotContain("bootsharp/generated.g.d.mts", "Record"); + DoesNotContain("bootsharp/generated.g.d.mts", "export function inv"); + DoesNotContain("bootsharp/generated.g.d.mts", "export let fun"); + } + + [Fact] + public void IgnoresImplementedInterfaceMethods () + { + AddAssembly(With( """ - export namespace Nya { - export interface Nya { + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] + + public interface IExportedStatic { int Foo () => 0; } + public interface IImportedStatic { int Foo () => 0; } + public interface IExportedInstanced { int Foo () => 0; } + public interface IImportedInstanced { int Foo () => 0; } + + public class Class + { + [Export] public static IExportedInstanced GetExported () => default; + [Import] public static IImportedInstanced GetImported () => default; + } + """)); + Execute(); + DoesNotContain("Foo"); + } + + [Fact] + public void DeclarationsCrossNamespaceImportsEmitted () + { + AddAssembly(With( + """ + namespace Metadata { public record Value (int X); } + namespace Syntax { + public class Class { + [Export] public static Metadata.Value Get () => default!; } } + """)); + Execute(); + Contains("syntax.g.d.mts", "import type * as metadata from \"./metadata.g.mjs\";"); + Contains("syntax.g.d.mts", "metadata.Value"); + } - export namespace Fun.Class { - export let onFun: (nya: Nya.Nya) => void; + [Fact] + public void DeclarationFileImportsRootNamespaceTypeFromPackageRoot () + { + AddAssembly(With( + """ + public record RootRecord (string Value); + namespace Space + { + public class Class { [Export] public static RootRecord Get () => default!; } } - """); + """)); + Execute(); + Contains("space.g.d.mts", "import type * as index from \"./index.g.mjs\";"); + Contains("space.g.d.mts", "index.RootRecord"); } [Fact] - public void RespectsSpacePrefInModules () + public void TypeDeclarationGroupsMultipleNestedTypes () { AddAssembly(With( """ - [assembly:Preferences(Space = [@".+", "Foo"])] - [assembly:Export(typeof(Space.IExported))] - [assembly:Import(typeof(Space.IImported))] + public class Outer { public record A (int X); public record B (int Y); } + public class Other { public record C (int Z); } + public class Class + { + [Export] public static Outer.A GetA () => default!; + [Export] public static Outer.B GetB () => default!; + [Export] public static Other.C GetC () => default!; + } + """)); + Execute(); + Contains("export namespace Other {"); + Contains("export namespace Outer {"); + } + + [Fact] + public void RespectsPrefsInStatics () + { + AddAssembly(With( + """ + [assembly:Preferences( + Space = [@".+", "index"], + Name = [@"^Class$", "Foo"], + Method = [@"^Method$", "bar"], + Property = [@"^Property$", "baz"], + Event = [@"^Event$", "qux"] + )] namespace Space; public enum Enum { A, B } - public interface IExported { void Inv (string s, Enum e); } - public interface IImported { void Fun (string s, Enum e); } + public class Class + { + [Export] public static Enum Method () => default; + [Export] public static Enum Property { get; set; } + [Export] public static event Action? Event; + } """)); Execute(); Contains( """ export namespace Foo { - export enum Enum { - A, - B - } + export const qux: Event<[]>; + export let baz: Enum; + export function bar(): Enum; } - - export namespace Foo { - export function inv(s: string, e: Foo.Enum): void; - export let fun: (s: string, e: Foo.Enum) => void; + export enum Enum { + A, + B } """); } [Fact] - public void RespectsTypePreference () + public void RespectsPrefsInModules () { AddAssembly(With( """ - [assembly: Bootsharp.Preferences( - Type = [@"Record", "Foo", @".+`.+", "Bar"] + [assembly:Preferences( + Space = [@".+", "index"], + Name = [@"^I.+$", "Foo"], + Method = [@"^Inv$", "bar", @"^Fun$", "baz"], + Property = [@"^State$", "qux"], + Event = [@"^Changed$", "quz"] )] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] - public record Record; - public record Generic; + namespace Space; - public class Class + public enum Enum { A, B } + + public interface IExported + { + Enum State { get; set; } + event Action? Changed; + void Inv (Enum e); + } + public interface IImported { - [Export] public static void Inv (Record r, Generic g) {} + void Fun (Enum e); } """)); Execute(); Contains( """ - export type Bar = Readonly<{ - }>; - export type Foo = Readonly<{ - }>; - - export namespace Class { - export function inv(r: Foo, g: Bar): void; + export namespace Foo { + export const quz: Event<[]>; + export let qux: Enum; + export function bar(e: Enum): void; + export let baz: (e: Enum) => void; + } + export enum Enum { + A, + B } """); } [Fact] - public void IgnoresBindingsInGeneratedNamespace () - { - AddAssembly(With("Bootsharp.Generated", - """ - public record Record; - public static class Exports { [Export] public static void Inv (Record r) {} } - public static class Imports { [Import] public static void Fun () {} } - """)); - Execute(); - DoesNotContain("Record"); - DoesNotContain("export function inv"); - DoesNotContain("export let fun"); - } - - [Fact] - public void IgnoresImplementedInterfaceMethods () + public void RespectsPrefsInInstanced () { AddAssembly(With( """ - [assembly:Export(typeof(IExportedStatic))] - [assembly:Import(typeof(IImportedStatic))] + [assembly:Preferences( + Space = [@".+", "index"], + Name = [@"^IInst$", "Foo"], + Method = [@"^Method$", "bar"], + Property = [@"^Property$", "baz"], + Event = [@"^Event$", "qux"] + )] - public interface IExportedStatic { int Foo () => 0; } - public interface IImportedStatic { int Foo () => 0; } - public interface IExportedInstanced { int Foo () => 0; } - public interface IImportedInstanced { int Foo () => 0; } + namespace Space; + + public enum Enum { A, B } + + public interface IInst + { + Enum Property { get; set; } + event Action? Event; + void Method (Enum e); + } public class Class { - [Export] public static IExportedInstanced GetExported () => default; - [Import] public static IImportedInstanced GetImported () => default; + [Export] public static IInst Get () => default; } """)); Execute(); - DoesNotContain("Foo"); + Contains( + """ + export namespace Class { + export function get(): Foo; + } + export interface Foo { + qux: Event<[]>; + baz: Enum; + bar(e: Enum): void; + } + export enum Enum { + A, + B + } + """); } [Fact] @@ -1340,7 +1437,7 @@ public enum Kind /// A payload sent across interop. /// /// Visible in generated TypeScript. - public record Payload + public record Payload { /// The payload name. public string Name { get; init; } @@ -1354,7 +1451,7 @@ public class HandlerArgs : EventArgs; /// Payload changed callback. /// Payload from custom delegate. /// Label from custom delegate. - public delegate void PayloadChanged (Payload payload, string label); + public delegate void PayloadChanged (Payload payload, string label); /// /// Exported instance API. @@ -1392,14 +1489,14 @@ public partial class Class [Export] public static int Foo (List function, string[] names) => 0; /// Gets payload. - [Export] public static Payload Get (Kind kind) => default; + [Export] public static Payload Get (Kind kind) => default; /// Gets exported instance. [Export] public static IExportedInstanced GetExported () => default; /// Receives foo. /// Count to receive. - [Import] public static void OnFoo (int count) { } + [Import] public static void OnFoo (Payload count) { } /// Value without summary. [Import] public static void OnParamOnly (string value) { } @@ -1427,7 +1524,7 @@ export enum Kind { /** * A payload sent across interop. */ - export type Payload = Readonly<{ + export type Payload = Readonly<{ /** * The payload name. */ @@ -1460,23 +1557,23 @@ export namespace Class { /** * Exports completion signal. */ - export const expEvt: EventSubscriber<[obj: boolean]>; + export const expEvt: Event<[obj: boolean]>; /** * Imports completion signal. */ - export const impEvt: EventBroadcaster<[arg1: string, arg2: number]>; + export const impEvt: Event<[arg1: string, arg2: number]>; /** * Exports payload changes. * @param payload Payload from custom delegate. * @param label Label from custom delegate. */ - export const payloadChanged: EventSubscriber<[payload: Payload, label: string]>; + export const payloadChanged: Event<[payload: Payload, label: string]>; /** * Imports handler signal. * @param sender Sender from event handler. * @param e Payload from event handler. */ - export const handlerEvt: EventBroadcaster<[sender: any | undefined, e: HandlerArgs]>; + export const handlerEvt: Event<[sender: any | undefined, e: HandlerArgs]>; /** * Runs foo. * @param fn Function value. @@ -1487,7 +1584,7 @@ export namespace Class { /** * Gets payload. */ - export function get(kind: Kind): Payload; + export function get(kind: Kind): Payload; /** * Gets exported instance. */ @@ -1496,7 +1593,7 @@ export namespace Class { * Receives foo. * @param count Count to receive. */ - export let onFoo: (count: number) => void; + export let onFoo: (count: Payload) => void; /** * @param value Value without summary. */ diff --git a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/GenerateJSTest.cs similarity index 82% rename from src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateJS/GenerateJSTest.cs index 2ca92d3c..586063ca 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/GenerateJSTest.cs @@ -1,17 +1,16 @@ namespace Bootsharp.Publish.Test; -public class PackTest : TaskTest +public class GenerateJSTest : TaskTest { - protected BootsharpPack Task { get; } + protected GenerateJS Task { get; } protected byte[] MockWasmBinary { get; } = "MockWasmContent"u8.ToArray(); protected string MockDotNetContent { get; } = "MockDotNetContent"; protected string MockRuntimeContent { get; } = "MockRuntimeContent"; protected string MockNativeContent { get; } = "MockNativeContent"; - protected string GeneratedBindings => ReadProjectFile("generated/bindings.g.mjs"); - protected string GeneratedDeclarations => ReadProjectFile("generated/bindings.g.d.mts"); protected string GeneratedResources => ReadProjectFile("generated/resources.g.mjs"); + protected override string TestedDirectory => "generated"; - public PackTest () + public GenerateJSTest () { Task = CreateTask(); Project.WriteFile("dotnet.js", MockDotNetContent); @@ -35,7 +34,7 @@ protected override void AddAssembly (string assemblyName, params MockSource[] so Project.WriteFile(assemblyName[..^3] + "wasm", ""); } - private BootsharpPack CreateTask () => new() { + private GenerateJS CreateTask () => new() { BuildDirectory = Project.Root, DebugDirectory = Project.Root, InspectedDirectory = Project.Root, diff --git a/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/InspectionTest.cs similarity index 98% rename from src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateJS/InspectionTest.cs index 8e950063..9e0fb828 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/InspectionTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class SolutionInspectionTest : PackTest +public class InspectionTest : GenerateJSTest { [Fact] public void AllAssembliesAreInspected () diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs new file mode 100644 index 00000000..81803650 --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSInstanceTest.cs @@ -0,0 +1,259 @@ +namespace Bootsharp.Publish.Test; + +public class JSInstanceTest : GenerateJSTest +{ + protected override string TestedContent { get => field ?? ReadProjectFile("generated/instances.g.mjs") ?? ""; set; } + + [Fact] + public void GeneratesForMethods () + { + AddAssembly(With( + """ + public record Info (string Value); + + public interface IExported { Info Inv (IExported it, Info info); } + public interface IImported { Info Fun (IImported it, Info info); } + + public partial class Class + { + [Export] public static Task GetExported (IImported it) => default; + [Import] public static Task GetImported (IExported it) => default; + } + """)); + Execute(); + Contains( + """ + $i.IExported = class JSExported { + constructor(_id) { this._id = _id; } + inv(it, info) { return index.IExported.inv(this._id, it, info); } + }; + """); + Contains("index.g.mjs", + """ + export const Class = { + getExported: async (it) => $i.resolve(await exports.Class_GetExported($i.import(it)), $i.IExported), + get getImported() { return this.getImportedHandler; }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = async (it) => $i.import(await this.getImportedHandler($i.resolve(it, $i.IExported))); }, + get getImportedSerialized() { return this.getImportedSerializedHandler; } + }; + 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) + }; + export const IExported = { + inv: (_id, it, info) => deserialize(exports.Bootsharp_Generated_Exports_JSExported_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) + }; + """); + } + + [Fact] + public void GeneratesForProperties () + { + AddAssembly(With( + """ + public record Info (string Value); + + public interface IExported + { + Info? State { get; set; } + IExported Exported { get; } + IImported Imported { set; } + } + + public interface IImported + { + Info? State { get; set; } + IImported Imported { get; } + IExported Exported { set; } + } + + public partial class Class + { + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; + } + """)); + Execute(); + Contains( + """ + $i.IExported = class JSExported { + constructor(_id) { this._id = _id; } + get state() { return index.IExported.getState(this._id); } + set state(value) { index.IExported.setState(this._id, value); } + get exported() { return index.IExported.getExported(this._id); } + set imported(value) { index.IExported.setImported(this._id, value); } + }; + """); + Contains("index.g.mjs", + """ + export const Class = { + getExported: (it) => $i.resolve(exports.Class_GetExported($i.import(it)), $i.IExported), + get getImported() { return this.getImportedHandler; }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (it) => $i.import(this.getImportedHandler($i.resolve(it, $i.IExported))); }, + get getImportedSerialized() { return this.getImportedSerializedHandler; } + }; + export const IImported = { + getStateSerialized(_id) { return serialize($i.imported(_id).state, $s.Info); }, + setStateSerialized(_id, value) { $i.imported(_id).state = deserialize(value, $s.Info); }, + getImportedSerialized(_id) { return $i.import($i.imported(_id).imported); }, + 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)); } + }; + """); + } + + [Fact] + public void GeneratesForEvents () + { + AddAssembly(With( + """ + public record Info (string Value); + + 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( + """ + $i.IExported = class JSExported { + constructor(_id) { this._id = _id; } + changed = new Event(); + broadcastChanged(arg1, arg2) { this.changed.broadcast(arg1, arg2); } + }; + """); + Contains( + """ + $i.import_IImported = function (it) { + return $i.import(it, _id => { + it.changed.subscribe(handleChanged); + return () => { + it.changed.unsubscribe(handleChanged); + }; + + function handleChanged(arg1, arg2) { exports.Bootsharp_Generated_Imports_JSImported_InvokeChanged(_id, $i.import_IImported(arg1), serialize(arg2, $s.Info)); } + }); + }; + """); + Contains("index.g.mjs", + """ + export const Class = { + getExported: (it) => $i.resolve(exports.Class_GetExported($i.import_IImported(it)), $i.IExported), + get getImported() { return this.getImportedHandler; }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (it) => $i.import_IImported(this.getImportedHandler($i.resolve(it, $i.IExported))); }, + get getImportedSerialized() { return this.getImportedSerializedHandler; } + }; + export const IImported = { + }; + export const IExported = { + broadcastChangedSerialized: (_id, arg1, arg2) => $i.resolve(_id, $i.IExported).broadcastChanged($i.resolve(arg1, $i.IExported), deserialize(arg2, $s.Info)) + }; + """); + } + + [Fact] + public void DoesNotEmitDuplicateSpecializedImporters () + { + 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(@"\$i\.import_IBi = function"); + } + + [Fact] + public void IgnoresImplementedInterfaceMethods () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] + + public interface IExportedStatic { int Foo () => 0; } + public interface IImportedStatic { int Foo () => 0; } + public interface IExportedInstanced { int Foo () => 0; } + public interface IImportedInstanced { int Foo () => 0; } + + public class Class + { + [Export] public static IExportedInstanced GetExported () => default; + [Import] public static IImportedInstanced GetImported () => default; + } + """)); + Execute(); + DoesNotContain("Foo"); + } + + [Fact] + public void ImportersDontLeakToModule () + { + AddAssembly(With( + """ + public interface IExportedInstanced { void Inv (); } + public interface IImportedInstanced { event Action? OnChanged; } + public class Class + { + [Export] public static IExportedInstanced GetExported () => default!; + [Import] public static IImportedInstanced GetImported () => default!; + } + """)); + Execute(); + Contains("$i.IExportedInstanced = class JSExportedInstanced"); + Contains("$i.import_IImportedInstanced = function"); + DoesNotContain("index.g.mjs", "$i.IExportedInstanced = class"); + DoesNotContain("index.g.mjs", "$i.import_IImportedInstanced = function"); + } + + [Fact] + public void CanReferenceObjectsFromOtherModules () + { + AddAssembly(With( + """ + namespace Foo.Bar; + + public interface IExported + { + int State { get; } + void Method (); + } + + public partial class Class + { + [Export] public static IExported Get () => default; + } + """)); + Execute(); + Contains( + """ + import * as foo_bar from "./foo/bar.g.mjs"; + + $i.Foo_Bar_IExported = class Foo_Bar_JSExported { + 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 new file mode 100644 index 00000000..2072d402 --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -0,0 +1,805 @@ +namespace Bootsharp.Publish.Test; + +public class JSModuleTest : GenerateJSTest +{ + protected override string TestedContent { get => field ?? ReadProjectFile("generated/index.g.mjs") ?? ""; set; } + + [Fact] + public void WhenDebugEnabledUsesExportImportHelpers () + { + Task.Debug = true; + AddAssembly(With( + """ + [assembly:Export(typeof(IExportedStatic))] + + public interface IExportedStatic { int State { get; set; } } + public interface IImportedInstanced { event Action? Changed; } + + public partial class Class + { + [Import] public static event Action? Evt; + [Export] public static Task InvAsync () => Task.FromResult(0); + [Export] public static void UseImported (IImportedInstanced it) {} + [Import] public static void Fun () {} + } + """)); + Execute(); + Contains("""getExport("Class_InvokeEvt")"""); + Contains("""getExport("Class_InvAsync")"""); + Contains("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetState")"""); + Contains("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); + } + + [Fact] + public void WhenDebugDisabledDoesNotUseExportImportHelpers () + { + Task.Debug = false; + AddAssembly(With( + """ + [assembly:Export(typeof(IExportedStatic))] + + public interface IExportedStatic { int State { get; set; } } + + public class Class + { + [Export] public static Task InvAsync () => Task.FromResult(0); + [Import] public static void Fun () {} + } + """)); + Execute(); + DoesNotContain("getExport"); + DoesNotContain("getImport"); + } + + [Fact] + public void BindingForStaticExportedMethodGenerated () + { + AddAssembly(WithClass("Foo.Bar", "[Export] public static void Nya () {}")); + Execute(); + Contains("foo/bar.g.mjs", + """ + export const Class = { + nya: () => exports.Foo_Bar_Class_Nya() + }; + """); + } + + [Fact] + public void BindingForStaticImportedMethodGenerated () + { + AddAssembly(WithClass("Foo.Bar", "[Import] public static void Fun () {}")); + Execute(); + Contains("foo/bar.g.mjs", + """ + export const Class = { + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + + [Fact] + public void BindingForStaticEventGenerated () + { + AddAssembly( + WithClass("[Export] public static event Action? ExpEvt;"), + WithClass("[Export] public static event Action? Evt;"), + WithClass("[Import] public static event Action? ImpEvt;")); + Execute(); + Contains( + """ + export const Class = { + expEvt: new Event(), + broadcastExpEvtSerialized: () => Class.expEvt.broadcast(), + evt: new Event(), + broadcastEvtSerialized: (obj) => Class.evt.broadcast(obj), + impEvt: importEvent((arg1, arg2) => exports.Class_InvokeImpEvt(arg1, arg2)) + }; + """); + } + + [Fact] + public void BindingForStaticPropertyGenerated () + { + AddAssembly( + WithClass("[Export] public static int ExpProp { get; set; }"), + WithClass("[Import] public static string ImpProp { get => default!; set { } }")); + Execute(); + Contains( + """ + export const Class = { + get expProp() { return exports.Class_GetExpProp(); }, + set expProp(value) { exports.Class_SetExpProp(value); }, + getImpPropSerialized() { return this.impProp.get(); }, + setImpPropSerialized(value) { this.impProp.set(value); } + }; + """); + } + + [Fact] + public void LibraryExportsNamespaceObject () + { + AddAssembly(WithClass("Foo", "[Export] public static void Bar () {}")); + Execute(); + Contains("foo.g.mjs", + """ + export const Class = { + bar: () => exports.Foo_Class_Bar() + }; + """); + } + + [Fact] + public void WhenSpaceContainDotsDirectoriesCreatedForEachPart () + { + AddAssembly(WithClass("Foo.Bar.Nya", "[Export] public static void Bar () {}")); + Execute(); + Contains("foo/bar/nya.g.mjs", + """ + export const Class = { + bar: () => exports.Foo_Bar_Nya_Class_Bar() + }; + """); + } + + [Fact] + public void WhenMultipleSpacesEachGetItsOwnModule () + { + AddAssembly( + WithClass("Foo", "[Export] public static void Foo () {}"), + WithClass("Bar.Nya", "[Import] public static void Fun () {}")); + Execute(); + Contains("foo.g.mjs", + """ + export const Class = { + foo: () => exports.Foo_Class_Foo() + }; + """); + Contains("bar/nya.g.mjs", + """ + export const Class = { + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + + [Fact] + public void WhenMultipleAssembliesWithEqualSpaceObjectDeclaredOnlyOnce () + { + AddAssembly(WithClass("Foo", "[Export] public static void Bar () {}")); + AddAssembly(WithClass("Foo", "[Import] public static void Fun () {}")); + Execute(); + Once("foo.g.mjs", "export const Class"); + } + + [Fact] + public void DifferentSpacesWithSameRootAssignedUnderSameDirectory () + { + AddAssembly( + WithClass("Nya.Foo", "[Export] public static void Foo () {}"), + WithClass("Nya.Bar", "[Import] public static void Fun () {}")); + Execute(); + Contains("nya/foo.g.mjs", + """ + export const Class = { + foo: () => exports.Nya_Foo_Class_Foo() + }; + """); + Contains("nya/bar.g.mjs", + """ + export const Class = { + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + + [Fact] + public void DifferentSpacesStartingEquallyAreNotAssignedToSameModule () + { + AddAssembly( + WithClass("Foo", "[Export] public static void Method () {}"), + WithClass("FooBar.Baz", "[Export] public static void Method () {}") + ); + Execute(); + Contains("foo.g.mjs", + """ + export const Class = { + method: () => exports.Foo_Class_Method() + }; + """); + Contains("foo-bar/baz.g.mjs", + """ + export const Class = { + method: () => exports.FooBar_Baz_Class_Method() + }; + """); + } + + [Fact] + public void BindingsFromMultipleSpacesAssignedToRespectiveModules () + { + AddAssembly(WithClass("Foo", "[Export] public static int Foo () => 0;")); + AddAssembly(WithClass("Bar.Nya", "[Import] public static void Fun () {}")); + Execute(); + Contains("bar/nya.g.mjs", + """ + export const Class = { + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + Contains("foo.g.mjs", + """ + export const Class = { + foo: () => exports.Foo_Class_Foo() + }; + """); + } + + [Fact] + public void BindingsFromMultipleClassesAssignedToRespectiveModules () + { + AddAssembly( + With("public class ClassA { [Export] public static void Inv () {} }"), + With("public class ClassB { [Import] public static void Fun () {} }")); + Execute(); + Contains( + """ + export const ClassA = { + inv: () => exports.ClassA_Inv() + }; + export const ClassB = { + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + + [Fact] + public void WhenNoSpaceBindingsAreAssignedToRootModule () + { + AddAssembly( + WithClass("[Export] public static Task Nya () => Task.FromResult(0);"), + WithClass("[Import] public static void Fun () {}")); + Execute(); + Contains( + """ + export const Class = { + nya: () => exports.Class_Nya(), + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + + [Fact] + public void VariablesConflictingWithJSTypesAreRenamed () + { + AddAssembly(WithClass("[Export] public static void Fun (string function) {}")); + Execute(); + Contains( + """ + export const Class = { + fun: (fn) => exports.Class_Fun(fn) + }; + """); + } + + [Fact] + public void SerializesUserType () + { + AddAssembly( + With("public record Info (DateTimeOffset Date, nint Ptr, Info? Self);"), + WithClass("[Export] public static event Action? ExpEvt;"), + WithClass("[Import] public static event Action? ImpEvt;"), + WithClass("[Export] public static Info Foo (Info i) => default;"), + WithClass("[Import] public static Info? Bar (Info? i) => default;")); + Execute(); + Contains( + """ + export const Class = { + expEvt: new Event(), + broadcastExpEvtSerialized: (arg1, arg2) => Class.expEvt.broadcast(deserialize(arg1, $s.InfoArray) ?? undefined, deserialize(arg2, $s.Info)), + impEvt: importEvent((arg1, arg2) => exports.Class_InvokeImpEvt(arg1, serialize(arg2, $s.Info))), + foo: (i) => deserialize(exports.Class_Foo(serialize(i, $s.Info)), $s.Info), + get bar() { return this.barHandler; }, + set bar(handler) { this.barHandler = handler; this.barSerializedHandler = (i) => serialize(this.barHandler(deserialize(i, $s.Info)), $s.Info); }, + get barSerialized() { return this.barSerializedHandler; } + }; + """); + } + + [Fact] + public void AwaitsWhenSerializingInAsyncFunctions () + { + AddAssembly( + With("public record Info;"), + WithClass("[Export] public static Task Foo (Info i) => default;"), + WithClass("[Import] public static Task Bar (Info? i) => default;"), + WithClass("[Export] public static Task> Baz () => default;"), + WithClass("[Import] public static Task> Yaz () => default;")); + Execute(); + Contains( + """ + export const Class = { + foo: async (i) => deserialize(await exports.Class_Foo(serialize(i, $s.Info)), $s.Info), + get bar() { return this.barHandler; }, + set bar(handler) { this.barHandler = handler; this.barSerializedHandler = async (i) => serialize(await this.barHandler(deserialize(i, $s.Info)), $s.Info); }, + get barSerialized() { return this.barSerializedHandler; }, + baz: async () => deserialize(await exports.Class_Baz(), $s.System_Collections_Generic_IReadOnlyList_Of_Info), + get yaz() { return this.yazHandler; }, + set yaz(handler) { this.yazHandler = handler; this.yazSerializedHandler = async () => serialize(await this.yazHandler(), $s.System_Collections_Generic_IReadOnlyList_Of_Info); }, + get yazSerialized() { return this.yazSerializedHandler; } + }; + """); + } + + [Fact] + public void ExportedEnumsAreDeclaredInJS () + { + AddAssembly( + WithClass("n", "public enum Foo { A, B }"), + WithClass("n", "[Export] public static Foo GetFoo () => default;")); + Execute(); + Contains("n.g.mjs", + """ + export const Class = { + getFoo: () => deserialize(exports.n_Class_GetFoo(), $s.n_Class_Foo), + Foo: { + "0": "A", + "1": "B", + "A": 0, + "B": 1 + } + }; + """); + } + + [Fact] + public void DoesntDeclareSystemEnums () + { + AddAssembly( + WithClass("n", "public enum Foo { A, B }"), + WithClass("n", "[Export] public static Task GetFoo () => default;")); + Execute(); + TestedContent = ReadProjectFile("generated/n.g.mjs"); + Contains("Foo"); + DoesNotContain("LayoutKind"); + DoesNotContain("SecurityRuleSet"); + DoesNotContain("MethodAttributes"); + DoesNotContain("MethodImplAttributes"); + } + + [Fact] + public void CustomEnumIndexesArePreservedInJS () + { + AddAssembly( + With("n", "public enum Foo { A = 1, B = 6 }"), + WithClass("n", "[Export] public static Foo GetFoo () => default;")); + Execute(); + Contains("n.g.mjs", + """ + export const Class = { + getFoo: () => deserialize(exports.n_Class_GetFoo(), $s.n_Foo) + }; + export const Foo = { + "1": "A", + "6": "B", + "A": 1, + "B": 6 + }; + """); + } + + [Fact] + public void IgnoresBindingsInGeneratedNamespace () + { + AddAssembly(With("Bootsharp.Generated", + """ + public static class Exports { [Export] public static void Inv () {} } + public static class Imports { [Import] public static void Fun () {} } + """)); + Execute(); + DoesNotContain("bootsharp/generated.g.mjs", "inv: () =>"); + DoesNotContain("bootsharp/generated.g.mjs", "get fun()"); + } + + [Fact] + public void GeneratesForMethodsInModules () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public record Info (string Value); + + public interface IExported { Info Inv (string str, Info info); } + public interface IImported { Info Fun (string str, Info info); } + """)); + Execute(); + 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) + }; + export const IImported = { + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = (str, info) => serialize(this.funHandler(str, deserialize(info, $s.Space_Info)), $s.Space_Info); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + + [Fact] + public void GeneratesForPropertiesInModules () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public record Info (string Value); + + public interface IExported + { + Info? State { get; set; } + int Count { set; } + } + + public interface IImported + { + Info? State { get; set; } + int Count { set; } + } + """)); + Execute(); + 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); } + }; + export const IImported = { + getStateSerialized() { return serialize(this.state.get(), $s.Space_Info); }, + setStateSerialized(value) { this.state.set(deserialize(value, $s.Space_Info)); }, + setCountSerialized(value) { this.count.set(value); } + }; + """); + } + + [Fact] + public void GeneratesForEventsInModules () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public record Info (string Value); + + public interface IExported { event Action Evt; } + public interface IImported { event Action Evt; } + """)); + Execute(); + Contains("space.g.mjs", + """ + export const IExported = { + evt: new Event(), + 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))) + }; + """); + } + + [Fact] + public void ImportsAllModules () + { + AddAssembly( + WithClass("Foo.Bar", "[Export] public static void A () {}"), + WithClass("Baz", "[Import] public static void B () {}")); + Execute(); + Contains("imports.g.mjs", """import * as foo_bar from "./foo/bar.g.mjs";"""); + Contains("imports.g.mjs", """import * as baz from "./baz.g.mjs";"""); + Contains("imports.g.mjs", """runtime.setModuleImports("foo/bar", foo_bar);"""); + Contains("imports.g.mjs", """runtime.setModuleImports("baz", baz);"""); + } + + [Fact] + public void NamespaceBindingFileExistsPerNamespace () + { + AddAssembly( + WithClass("Foo.Bar", "[Export] public static void A () {}"), + WithClass("Baz", "[Export] public static void B () {}"), + WithClass("[Export] public static void Root () {}")); + Execute(); + Assert.NotNull(ReadProjectFile("generated/foo/bar.g.mjs")); + Assert.NotNull(ReadProjectFile("generated/baz.g.mjs")); + Assert.NotNull(ReadProjectFile("generated/index.g.mjs")); + } + + [Fact] + public void NestedGlobalEnumEmittedInIndexModule () + { + AddAssembly(With( + """ + public class Outer { public enum Inner { A, B } } + public class Class { [Export] public static Outer.Inner Get () => default!; } + """)); + Execute(); + Contains( + """ + export const Class = { + get: () => deserialize(exports.Class_Get(), $s.Outer_Inner) + }; + export const Outer = { + Inner: { + "0": "A", + "1": "B", + "A": 0, + "B": 1 + } + }; + """); + } + + [Fact] + public void NestedEnumUnderNamespaceEmittedInSpaceModule () + { + AddAssembly(With( + """ + namespace n; + public class Class { public enum Foo { A, B } } + public class Holder { [Export] public static Class.Foo Get () => default!; } + """)); + Execute(); + Contains("n.g.mjs", + """ + export const Holder = { + get: () => deserialize(exports.n_Holder_Get(), $s.n_Class_Foo) + }; + export const Class = { + Foo: { + "0": "A", + "1": "B", + "A": 0, + "B": 1 + } + }; + """); + } + + [Fact] + public void DeeplyNestedEnumIsEmitted () + { + AddAssembly(With( + """ + public class A + { + public class B + { + public enum C { X, Y } + } + } + public class Class + { + [Export] public static A.B.C Get () => default; + } + """)); + Execute(); + Contains( + """ + export const A = { + B: { + C: { + "0": "X", + "1": "Y", + "X": 0, + "Y": 1 + } + } + }; + """); + } + + [Fact] + public void NestedEnumsAreNotDeclaredTopLevel () + { + AddAssembly(With( + """ + public class Outer { public enum Nested { A, B } } + public class Class { [Export] public static Outer.Nested Get () => default!; } + """)); + Execute(); + Contains("Nested:"); + DoesNotContain("export const Nested"); + } + + [Fact] + public void DoesNotEmitObjectsForUnrelatedTypes () + { + AddAssembly(With( + """ + public record Record; + public class Outer { public record NestedRecord; } + public class Class + { + public record InnerRecord; + [Export] public static void Foo (Record record) {} + [Export] public static void Bar (Outer.NestedRecord nested) {} + [Export] public static void Baz (InnerRecord inner) {} + } + """)); + Execute(); + DoesNotContain("export const Record"); + DoesNotContain("export const NestedRecord"); + DoesNotContain("export const InnerRecord"); + DoesNotContain("export const Outer"); + DoesNotContain("Record:"); + DoesNotContain("NestedRecord:"); + DoesNotContain("InnerRecord:"); + DoesNotContain("Outer:"); + } + + [Fact] + public void RespectsPrefsInStatics () + { + AddAssembly(With( + """ + [assembly:Preferences( + Space = [@".+", "index"], + Name = [@"^Class$", "Foo"], + Method = [@"^Method$", "bar"], + Property = [@"^Property$", "baz"], + Event = [@"^Event$", "qux"] + )] + + namespace Space; + + public enum Enum { A, B } + + public class Class + { + [Export] public static Enum Method () => default; + [Export] public static Enum Property { get; set; } + [Export] public static event Action? Event; + } + """)); + Execute(); + Contains( + """ + export const Foo = { + qux: new Event(), + broadcastEventSerialized: () => Foo.qux.broadcast(), + get baz() { return deserialize(exports.Space_Class_GetProperty(), $s.Space_Enum); }, + set baz(value) { exports.Space_Class_SetProperty(serialize(value, $s.Space_Enum)); }, + bar: () => deserialize(exports.Space_Class_Method(), $s.Space_Enum) + }; + export const Enum = { + "0": "A", + "1": "B", + "A": 0, + "B": 1 + }; + """); + } + + [Fact] + public void RespectsPrefsInModules () + { + AddAssembly(With( + """ + [assembly:Preferences( + Space = [@".+", "index"], + Name = [@"^I.+$", "Foo"], + Method = [@"^Inv$", "bar", @"^Fun$", "baz"], + Property = [@"^State$", "qux"], + Event = [@"^Changed$", "quz"] + )] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public enum Enum { A, B } + + public interface IExported + { + Enum State { get; set; } + event Action? Changed; + void Inv (Enum e); + } + public interface IImported + { + void Fun (Enum e); + } + """)); + Execute(); + Contains( + """ + 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 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; } + }; + export const Enum = { + "0": "A", + "1": "B", + "A": 0, + "B": 1 + }; + """); + } + + [Fact] + public void RespectsPrefsInInstanced () + { + AddAssembly(With( + """ + [assembly:Preferences( + Space = [@".+", "index"], + Name = [@"^IInst$", "Foo"], + Method = [@"^Method$", "bar"], + Property = [@"^Property$", "baz"], + Event = [@"^Event$", "qux"] + )] + + namespace Space; + + public enum Enum { A, B } + + public interface IInst + { + Enum Property { get; set; } + event Action? Event; + void Method (Enum e); + } + + public class Class + { + [Export] public static IInst Get () => default; + } + """)); + Execute(); + Contains( + """ + export const Class = { + get: () => $i.resolve(exports.Space_Class_Get(), $i.Space_IInst) + }; + 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)) + }; + export const Enum = { + "0": "A", + "1": "B", + "A": 0, + "B": 1 + }; + """); + } +} diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSSerializerTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSSerializerTest.cs new file mode 100644 index 00000000..468dc4bf --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSSerializerTest.cs @@ -0,0 +1,114 @@ +namespace Bootsharp.Publish.Test; + +public class JSSerializerTest : GenerateJSTest +{ + protected override string TestedContent { get => field ?? ReadProjectFile("generated/serializer.g.mjs") ?? ""; set; } + + [Fact] + public void SerializesPrimitivesUnderUserType () + { + AddAssembly(With( + """ + namespace Space; + + public struct Structure; + public enum Enumeration { A, B } + + public record Node( + bool Boolean, + byte Byte, + sbyte SByte, + short Int16, + ushort UInt16, + uint UInt32, + long Int64, + ulong UInt64, + float Single, + decimal Decimal, + char Char, + string String, + DateTime DateTime, + DateTimeOffset DateTimeOffset, + nint NInt, + int Int, + int? NullableInt, + Structure Struct, + Structure? NullableStruct, + Enumeration Enum, + Enumeration? NullableEnum); + + public class Class + { + [Export] public static Node Echo (Node node) => node; + } + """)); + Execute(); + Contains("$s.System_Boolean = $s.std.Boolean;"); + Contains("$s.System_Byte = $s.std.Byte;"); + Contains("$s.System_SByte = $s.std.SByte;"); + Contains("$s.System_Int16 = $s.std.Int16;"); + Contains("$s.System_UInt16 = $s.std.UInt16;"); + Contains("$s.System_UInt32 = $s.std.UInt32;"); + Contains("$s.System_Int64 = $s.std.Int64;"); + Contains("$s.System_UInt64 = $s.std.UInt64;"); + Contains("$s.System_Single = $s.std.Single;"); + Contains("$s.System_Decimal = $s.std.Decimal;"); + Contains("$s.System_Char = $s.std.Char;"); + Contains("$s.System_String = $s.std.String;"); + Contains("$s.System_DateTime = $s.std.DateTime;"); + Contains("$s.System_DateTimeOffset = $s.std.DateTimeOffset;"); + Contains("$s.System_IntPtr = $s.std.IntPtr;"); + Contains("$s.System_Int32 = $s.std.Int32;"); + Contains("$s.System_Int32OrNull = $s.std.Nullable($s.System_Int32);"); + Contains("$s.Space_Structure = $s.binary(write_Space_Structure, read_Space_Structure);"); + Contains("$s.Space_StructureOrNull = $s.std.Nullable($s.Space_Structure);"); + Contains("$s.Space_Enumeration = $s.std.Int32;"); + Contains("$s.Space_EnumerationOrNull = $s.std.Nullable($s.Space_Enumeration);"); + } + + [Fact] + public void OrdersSelfReferencedElementBeforeCollection () + { + AddAssembly(With( + """ + public record Node (List? Children); + + public class Class + { + [Export] public static Node Echo (Node node) => node; + } + """)); + Execute(); + Contains( + """ + $s.Node = $s.binary(write_Node, read_Node); + $s.System_Collections_Generic_List_Of_Node = $s.std.List($s.Node); + """); + } + + [Fact] + public void SerializerConstantsLiveInSerializerFile () + { + AddAssembly(With( + """ + public record Info (string Value); + public class Class { [Export] public static Info Echo (Info i) => i; } + """)); + Execute(); + Contains("$s.Info = "); + DoesNotContain("index.g.mjs", "$s.Info = "); + } + + [Fact] + public void SerializedImportedInstanceImporterIsReferencedFromSerializerFile () + { + AddAssembly(With( + """ + public interface IImported { event Action? Changed; } + public record Wrapper (IImported Inner); + public class Class { [Export] public static Wrapper Echo (Wrapper w) => w; } + """)); + Execute(); + Contains("$i.import_IImported"); + } +} diff --git a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/ResourceTest.cs similarity index 97% rename from src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs rename to src/cs/Bootsharp.Publish.Test/GenerateJS/ResourceTest.cs index 5832023c..956fb2dc 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/ResourceTest.cs @@ -1,6 +1,6 @@ namespace Bootsharp.Publish.Test; -public class ResourceTest : PackTest +public class ResourceTest : GenerateJSTest { protected override string TestedContent => GeneratedResources; diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs deleted file mode 100644 index b316f102..00000000 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ /dev/null @@ -1,905 +0,0 @@ -namespace Bootsharp.Publish.Test; - -public class BindingTest : PackTest -{ - protected override string TestedContent => GeneratedBindings; - - [Fact] - public void WhenNoBindingsNothingGenerated () - { - Execute(); - Assert.Empty(TestedContent); - } - - [Fact] - public void WhenDebugEnabledEmitsAndUsesExportImportHelpers () - { - Task.Debug = true; - AddAssembly(With( - """ - [assembly:Export(typeof(IExportedStatic))] - - public interface IExportedStatic { int State { get; set; } } - public interface IImportedInstanced { event Action? Changed; } - - public partial class Class - { - [Import] public static event Action? Evt; - [Export] public static Task InvAsync () => Task.FromResult(0); - [Export] public static void UseImported (IImportedInstanced it) {} - [Import] public static void Fun () {} - } - """)); - Execute(); - Contains("function getExport"); - Contains("function getImport"); - Contains("""getExport("Class_InvokeEvt")"""); - Contains("""getExport("Class_InvAsync")"""); - Contains("""getExport("Bootsharp_Generated_Imports_JSImportedInstanced_InvokeChanged")"""); - Contains("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState")"""); - Contains("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); - } - - [Fact] - public void WhenDebugDisabledDoesntEmitAndDoesntUseExportImportHelpers () - { - Task.Debug = false; - AddAssembly(With( - """ - [assembly:Export(typeof(IExportedStatic))] - - public interface IExportedStatic { int State { get; set; } } - - public class Class - { - [Export] public static Task InvAsync () => Task.FromResult(0); - [Import] public static void Fun () {} - } - """)); - Execute(); - DoesNotContain("function getExport"); - DoesNotContain("function getImport"); - DoesNotContain("""getExport("Class_InvAsync")"""); - DoesNotContain("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState")"""); - DoesNotContain("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); - } - - [Fact] - public void BindingForStaticExportedMethodGenerated () - { - AddAssembly(WithClass("Foo.Bar", "[Export] public static void Nya () {}")); - Execute(); - Contains( - """ - export const Foo = { - Bar: { - Class: { - nya: () => exports.Foo_Bar_Class_Nya() - } - } - }; - """); - } - - [Fact] - public void BindingForStaticImportedMethodGenerated () - { - AddAssembly(WithClass("Foo.Bar", "[Import] public static void Fun () {}")); - Execute(); - Contains( - """ - export const Foo = { - Bar: { - Class: { - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - } - } - }; - """); - } - - [Fact] - public void BindingForStaticEventGenerated () - { - AddAssembly( - WithClass("[Export] public static event Action? ExpEvt;"), - WithClass("[Export] public static event Action? Evt;"), - WithClass("[Import] public static event Action? ImpEvt;")); - Execute(); - Contains( - """ - export const Class = { - expEvt: new Event(), - broadcastExpEvtSerialized: () => Class.expEvt.broadcast(), - evt: new Event(), - broadcastEvtSerialized: (obj) => Class.evt.broadcast(obj), - impEvt: importEvent((arg1, arg2) => exports.Class_InvokeImpEvt(arg1, arg2)) - }; - """); - } - - [Fact] - public void BindingForStaticPropertyGenerated () - { - AddAssembly( - WithClass("[Export] public static int ExpProp { get; set; }"), - WithClass("[Import] public static string ImpProp { get => default!; set { } }")); - Execute(); - Contains( - """ - export const Class = { - get expProp() { return exports.Class_GetPropertyExpProp(); }, - set expProp(value) { exports.Class_SetPropertyExpProp(value); }, - getPropertyImpPropSerialized() { return this.impProp.get(); }, - setPropertyImpPropSerialized(value) { this.impProp.set(value); } - }; - """); - } - - [Fact] - public void LibraryExportsNamespaceObject () - { - AddAssembly(WithClass("Foo", "[Export] public static void Bar () {}")); - Execute(); - Contains( - """ - export const Foo = { - Class: { - bar: () => exports.Foo_Class_Bar() - } - }; - """); - } - - [Fact] - public void WhenSpaceContainDotsObjectCreatedForEachPart () - { - AddAssembly(WithClass("Foo.Bar.Nya", "[Export] public static void Bar () {}")); - Execute(); - Contains( - """ - export const Foo = { - Bar: { - Nya: { - Class: { - bar: () => exports.Foo_Bar_Nya_Class_Bar() - } - } - } - }; - """); - } - - [Fact] - public void WhenMultipleSpacesEachGetItsOwnObject () - { - AddAssembly( - WithClass("Foo", "[Export] public static void Foo () {}"), - WithClass("Bar.Nya", "[Import] public static void Fun () {}")); - Execute(); - Contains( - """ - export const Bar = { - Nya: { - Class: { - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - } - } - }; - export const Foo = { - Class: { - foo: () => exports.Foo_Class_Foo() - } - }; - """); - } - - [Fact] - public void WhenMultipleAssembliesWithEqualSpaceObjectDeclaredOnlyOnce () - { - AddAssembly(WithClass("Foo", "[Export] public static void Bar () {}")); - AddAssembly(WithClass("Foo", "[Import] public static void Fun () {}")); - Execute(); - Once("export const Foo"); - Contains("bar: () => exports.Foo_Class_Bar()"); - Contains( - """ - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - """); - } - - [Fact] - public void DifferentSpacesWithSameRootAssignedUnderSameObject () - { - AddAssembly( - WithClass("Nya.Foo", "[Export] public static void Foo () {}"), - WithClass("Nya.Bar", "[Import] public static void Fun () {}")); - Execute(); - Contains( - """ - export const Nya = { - Bar: { - Class: { - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - } - }, - Foo: { - Class: { - foo: () => exports.Nya_Foo_Class_Foo() - } - } - }; - """); - } - - [Fact] - public void DifferentSpacesStartingEquallyAreNotAssignedToSameObject () - { - AddAssembly( - WithClass("Foo", "[Export] public static void Method () {}"), - WithClass("FooBar.Baz", "[Export] public static void Method () {}") - ); - Execute(); - Contains( - """ - export const Foo = { - Class: { - method: () => exports.Foo_Class_Method() - } - }; - export const FooBar = { - Baz: { - Class: { - method: () => exports.FooBar_Baz_Class_Method() - } - } - }; - """); - } - - [Fact] - public void BindingsFromMultipleSpacesAssignedToRespectiveObjects () - { - AddAssembly(WithClass("Foo", "[Export] public static int Foo () => 0;")); - AddAssembly(WithClass("Bar.Nya", "[Import] public static void Fun () {}")); - Execute(); - Contains( - """ - export const Bar = { - Nya: { - Class: { - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - } - } - }; - export const Foo = { - Class: { - foo: () => exports.Foo_Class_Foo() - } - }; - """); - } - - [Fact] - public void BindingsFromMultipleClassesAssignedToRespectiveObjects () - { - AddAssembly( - With("public class ClassA { [Export] public static void Inv () {} }"), - With("public class ClassB { [Import] public static void Fun () {} }")); - Execute(); - Contains( - """ - export const ClassA = { - inv: () => exports.ClassA_Inv() - }; - export const ClassB = { - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - }; - """); - } - - [Fact] - public void WhenNoSpaceBindingsAreAssignedToClassObject () - { - AddAssembly( - WithClass("[Export] public static Task Nya () => Task.FromResult(0);"), - WithClass("[Import] public static void Fun () {}")); - Execute(); - Contains( - """ - export const Class = { - nya: () => exports.Class_Nya(), - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - }; - """); - } - - [Fact] - public void VariablesConflictingWithJSTypesAreRenamed () - { - AddAssembly(WithClass("[Export] public static void Fun (string function) {}")); - Execute(); - Contains( - """ - export const Class = { - fun: (fn) => exports.Class_Fun(fn) - }; - """); - } - - [Fact] - public void SerializesUserType () - { - AddAssembly( - With("public record Info (DateTimeOffset Date, nint Ptr, Info? Self);"), - WithClass("[Export] public static event Action? ExpEvt;"), - WithClass("[Import] public static event Action? ImpEvt;"), - WithClass("[Export] public static Info Foo (Info i) => default;"), - WithClass("[Import] public static Info? Bar (Info? i) => default;")); - Execute(); - Contains( - """ - export const Class = { - expEvt: new Event(), - broadcastExpEvtSerialized: (arg1, arg2) => Class.expEvt.broadcast(deserialize(arg1, InfoArray) ?? undefined, deserialize(arg2, Info)), - impEvt: importEvent((arg1, arg2) => exports.Class_InvokeImpEvt(arg1, serialize(arg2, Info))), - foo: (i) => deserialize(exports.Class_Foo(serialize(i, Info)), Info), - get bar() { return this.barHandler; }, - set bar(handler) { this.barHandler = handler; this.barSerializedHandler = (i) => serialize(this.barHandler(deserialize(i, Info)), Info); }, - get barSerialized() { return this.barSerializedHandler; } - }; - """); - } - - [Fact] - public void SerializesPrimitivesUnderUserType () - { - AddAssembly(With( - """ - namespace Space; - - public struct Structure; - public enum Enumeration { A, B } - - public record Node( - bool Boolean, - byte Byte, - sbyte SByte, - short Int16, - ushort UInt16, - uint UInt32, - long Int64, - ulong UInt64, - float Single, - decimal Decimal, - char Char, - string String, - DateTime DateTime, - DateTimeOffset DateTimeOffset, - nint NInt, - int Int, - int? NullableInt, - Structure Struct, - Structure? NullableStruct, - Enumeration Enum, - Enumeration? NullableEnum); - - public class Class - { - [Export] public static Node Echo (Node node) => node; - } - """)); - Execute(); - Contains("const System_Boolean = types.Boolean;"); - Contains("const System_Byte = types.Byte;"); - Contains("const System_SByte = types.SByte;"); - Contains("const System_Int16 = types.Int16;"); - Contains("const System_UInt16 = types.UInt16;"); - Contains("const System_UInt32 = types.UInt32;"); - Contains("const System_Int64 = types.Int64;"); - Contains("const System_UInt64 = types.UInt64;"); - Contains("const System_Single = types.Single;"); - Contains("const System_Decimal = types.Decimal;"); - Contains("const System_Char = types.Char;"); - Contains("const System_String = types.String;"); - Contains("const System_DateTime = types.DateTime;"); - Contains("const System_DateTimeOffset = types.DateTimeOffset;"); - Contains("const System_IntPtr = types.IntPtr;"); - Contains("const System_Int32 = types.Int32;"); - Contains("const System_Int32OrNull = types.Nullable(System_Int32);"); - Contains("const Space_Structure = binary(write_Space_Structure, read_Space_Structure);"); - Contains("const Space_StructureOrNull = types.Nullable(Space_Structure);"); - Contains("const Space_Enumeration = types.Int32;"); - Contains("const Space_EnumerationOrNull = types.Nullable(Space_Enumeration);"); - } - - [Fact] - public void OrdersSelfReferencedElementBeforeCollection () - { - AddAssembly(With( - """ - public record Node (List? Children); - - public class Class - { - [Export] public static Node Echo (Node node) => node; - } - """)); - Execute(); - Contains( - """ - const Node = binary(write_Node, read_Node); - const System_Collections_Generic_List_Of_Node = types.List(Node); - """); - } - - [Fact] - public void AwaitsWhenSerializingInAsyncFunctions () - { - AddAssembly( - With("public record Info;"), - WithClass("[Export] public static Task Foo (Info i) => default;"), - WithClass("[Import] public static Task Bar (Info? i) => default;"), - WithClass("[Export] public static Task> Baz () => default;"), - WithClass("[Import] public static Task> Yaz () => default;")); - Execute(); - Contains( - """ - export const Class = { - foo: async (i) => deserialize(await exports.Class_Foo(serialize(i, Info)), Info), - get bar() { return this.barHandler; }, - set bar(handler) { this.barHandler = handler; this.barSerializedHandler = async (i) => serialize(await this.barHandler(deserialize(i, Info)), Info); }, - get barSerialized() { return this.barSerializedHandler; }, - baz: async () => deserialize(await exports.Class_Baz(), System_Collections_Generic_IReadOnlyList_Of_Info), - get yaz() { return this.yazHandler; }, - set yaz(handler) { this.yazHandler = handler; this.yazSerializedHandler = async () => serialize(await this.yazHandler(), System_Collections_Generic_IReadOnlyList_Of_Info); }, - get yazSerialized() { return this.yazSerializedHandler; } - }; - """); - } - - [Fact] - public void ExportedEnumsAreDeclaredInJS () - { - AddAssembly( - WithClass("n", "public enum Foo { A, B }"), - WithClass("n", "[Export] public static Foo GetFoo () => default;")); - Execute(); - Contains( - """ - export const n = { - Class: { - getFoo: () => deserialize(exports.n_Class_GetFoo(), n_Class_Foo), - Foo: { "0": "A", "1": "B", "A": 0, "B": 1 } - } - }; - """); - } - - [Fact] - public void DoesntDeclareSystemEnums () - { - AddAssembly( - WithClass("n", "public enum Foo { A, B }"), - WithClass("n", "[Export] public static Task GetFoo () => default;")); - Execute(); - Contains("Foo"); - DoesNotContain("LayoutKind"); - DoesNotContain("SecurityRuleSet"); - DoesNotContain("MethodAttributes"); - DoesNotContain("MethodImplAttributes"); - } - - [Fact] - public void CustomEnumIndexesArePreservedInJS () - { - AddAssembly( - With("n", "public enum Foo { A = 1, B = 6 }"), - WithClass("n", "[Export] public static Foo GetFoo () => default;")); - Execute(); - Contains( - """ - export const n = { - Foo: { "1": "A", "6": "B", "A": 1, "B": 6 }, - Class: { - getFoo: () => deserialize(exports.n_Class_GetFoo(), n_Foo) - } - }; - """); - } - - [Fact] - public void RespectsSpacePreferenceInStaticMembers () - { - AddAssembly( - With( - """ - [assembly: Bootsharp.Preferences( - Space = [@"^Foo\.Bar\.(\S+)", "$1"] - )] - """), - WithClass("Foo.Bar.Nya", "[Export] public static Task GetNya () => Task.CompletedTask;"), - WithClass("Foo.Bar.Fun", "[Import] public static void OnFun () {}")); - Execute(); - Contains( - """ - export const Fun = { - Class: { - get onFun() { return this.onFunHandler; }, - set onFun(handler) { this.onFunHandler = handler; this.onFunSerializedHandler = () => this.onFunHandler(); }, - get onFunSerialized() { return this.onFunSerializedHandler; } - } - }; - export const Nya = { - Class: { - getNya: () => exports.Foo_Bar_Nya_Class_GetNya() - } - }; - """); - } - - [Fact] - public void RespectsSpacePreferenceInModules () - { - AddAssembly(With( - """ - [assembly:Preferences(Space = [@".+", "Foo"])] - [assembly:Export(typeof(Space.IExported))] - [assembly:Import(typeof(Space.IImported))] - - namespace Space; - - public interface IExported { void Inv (); } - public interface IImported { void Fun (); } - """)); - Execute(); - Contains( - """ - export const Foo = { - inv: () => exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(), - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, - get funSerialized() { return this.funSerializedHandler; } - }; - """); - } - - [Fact] - public void RespectsFunctionPreference () - { - AddAssembly( - With("""[assembly:Preferences(Function = [@".+", "foo"])]"""), - WithClass("Space", "[Export] public static void Inv () {}") - ); - Execute(); - Contains( - """ - export const Space = { - Class: { - foo: () => exports.Space_Class_Inv() - } - }; - """); - } - - [Fact] - public void IgnoresBindingsInGeneratedNamespace () - { - AddAssembly(With("Bootsharp.Generated", - """ - public static class Exports { [Export] public static void Inv () {} } - public static class Imports { [Import] public static void Fun () {} } - """)); - Execute(); - DoesNotContain("inv: () =>"); - DoesNotContain("get fun()"); - } - - [Fact] - public void GeneratesForMethodsInModules () - { - AddAssembly(With( - """ - [assembly:Export(typeof(Space.IExported))] - [assembly:Import(typeof(Space.IImported))] - - namespace Space; - - public record Info (string Value); - - public interface IExported { Info Inv (string str, Info info); } - public interface IImported { Info Fun (string str, Info info); } - """)); - Execute(); - Contains( - """ - export const Space = { - Exported: { - inv: (str, info) => deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(str, serialize(info, Space_Info)), Space_Info) - }, - Imported: { - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = (str, info) => serialize(this.funHandler(str, deserialize(info, Space_Info)), Space_Info); }, - get funSerialized() { return this.funSerializedHandler; } - } - }; - """); - } - - [Fact] - public void GeneratesForMethodsInInstanced () - { - AddAssembly(With( - """ - public record Info (string Value); - - public interface IExported { Info Inv (IExported it, Info info); } - public interface IImported { Info Fun (IImported it, Info info); } - - public partial class Class - { - [Export] public static Task GetExported (IImported it) => default; - [Import] public static Task GetImported (IExported it) => default; - } - """)); - Execute(); - Contains( - """ - class JSExported { - constructor(_id) { this._id = _id; } - inv(it, info) { return Exported.inv(this._id, it, info); } - } - - export const Class = { - getExported: async (it) => instances.export(await exports.Class_GetExported(instances.import(it)), id => new JSExported(id)), - get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = async (it) => instances.import(await this.getImportedHandler(instances.export(it, id => new JSExported(id)))); }, - get getImportedSerialized() { return this.getImportedSerializedHandler; } - }; - export const Exported = { - inv: (_id, it, info) => deserialize(exports.Bootsharp_Generated_Exports_JSExported_Inv(_id, it._id, serialize(info, Info)), Info) - }; - export const Imported = { - funSerialized: (_id, it, info) => serialize(instances.imported(_id).fun(instances.imported(it), deserialize(info, Info)), Info) - }; - """); - } - - [Fact] - public void GeneratesForPropertiesInModules () - { - AddAssembly(With( - """ - [assembly:Export(typeof(Space.IExported))] - [assembly:Import(typeof(Space.IImported))] - - namespace Space; - - public record Info (string Value); - - public interface IExported - { - Info? State { get; set; } - int Count { set; } - } - - public interface IImported - { - Info? State { get; set; } - int Count { set; } - } - """)); - Execute(); - Contains( - """ - export const Space = { - Exported: { - get state() { return deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_GetPropertyState(), Space_Info) ?? undefined; }, - set state(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetPropertyState(serialize(value, Space_Info)); }, - set count(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetPropertyCount(value); } - }, - Imported: { - getPropertyStateSerialized() { return serialize(this.state.get(), Space_Info); }, - setPropertyStateSerialized(value) { this.state.set(deserialize(value, Space_Info)); }, - setPropertyCountSerialized(value) { this.count.set(value); } - } - }; - """); - } - - [Fact] - public void GeneratesForPropertiesInInstanced () - { - AddAssembly(With( - """ - public record Info (string Value); - - public interface IExported - { - Info? State { get; set; } - IExported Exported { get; } - IImported Imported { set; } - } - - public interface IImported - { - Info? State { get; set; } - IImported Imported { get; } - IExported Exported { set; } - } - - public partial class Class - { - [Export] public static IExported GetExported (IImported it) => default; - [Import] public static IImported GetImported (IExported it) => default; - } - """)); - Execute(); - Contains( - """ - class JSExported { - constructor(_id) { this._id = _id; } - get state() { return Exported.getPropertyState(this._id); } - set state(value) { Exported.setPropertyState(this._id, value); } - get exported() { return Exported.getPropertyExported(this._id); } - set imported(value) { Exported.setPropertyImported(this._id, value); } - } - - export const Class = { - getExported: (it) => instances.export(exports.Class_GetExported(instances.import(it)), id => new JSExported(id)), - get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (it) => instances.import(this.getImportedHandler(instances.export(it, id => new JSExported(id)))); }, - get getImportedSerialized() { return this.getImportedSerializedHandler; } - }; - export const Exported = { - getPropertyState(_id) { return deserialize(exports.Bootsharp_Generated_Exports_JSExported_GetPropertyState(_id), Info) ?? undefined; }, - setPropertyState(_id, value) { exports.Bootsharp_Generated_Exports_JSExported_SetPropertyState(_id, serialize(value, Info)); }, - getPropertyExported(_id) { return instances.export(exports.Bootsharp_Generated_Exports_JSExported_GetPropertyExported(_id), id => new JSExported(id)); }, - setPropertyImported(_id, value) { exports.Bootsharp_Generated_Exports_JSExported_SetPropertyImported(_id, instances.import(value)); } - }; - export const Imported = { - getPropertyStateSerialized(_id) { return serialize(instances.imported(_id).state, Info); }, - setPropertyStateSerialized(_id, value) { instances.imported(_id).state = deserialize(value, Info); }, - getPropertyImportedSerialized(_id) { return instances.import(instances.imported(_id).imported); }, - setPropertyExportedSerialized(_id, value) { instances.imported(_id).exported = instances.export(value, id => new JSExported(id)); } - }; - """); - } - - [Fact] - public void GeneratesForEventsInModules () - { - AddAssembly(With( - """ - [assembly:Export(typeof(Space.IExported))] - [assembly:Import(typeof(Space.IImported))] - - namespace Space; - - public record Info (string Value); - - public interface IExported { event Action Evt; } - public interface IImported { event Action Evt; } - """)); - Execute(); - Contains( - """ - export const Space = { - Exported: { - evt: new Event(), - broadcastEvtSerialized: (obj) => Space.Exported.evt.broadcast(deserialize(obj, Space_Info)) - }, - Imported: { - evt: importEvent((obj) => exports.Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt(serialize(obj, Space_Info))) - } - }; - """); - } - - [Fact] - public void GeneratesForEventsInInstanced () - { - AddAssembly(With( - """ - public record Info (string Value); - - 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( - """ - function import_IImported(instance) { - return instances.import(instance, _id => { - instance.changed.subscribe(handleChanged); - return () => { - instance.changed.unsubscribe(handleChanged); - }; - - function handleChanged(arg1, arg2) { exports.Bootsharp_Generated_Imports_JSImported_InvokeChanged(_id, import_IImported(arg1), serialize(arg2, Info)); } - }); - } - """); - Contains( - """ - class JSExported { - constructor(_id) { this._id = _id; } - changed = new Event(); - broadcastChanged(arg1, arg2) { this.changed.broadcast(arg1, arg2); } - } - - export const Class = { - getExported: (it) => instances.export(exports.Class_GetExported(import_IImported(it)), id => new JSExported(id)), - get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (it) => import_IImported(this.getImportedHandler(instances.export(it, id => new JSExported(id)))); }, - get getImportedSerialized() { return this.getImportedSerializedHandler; } - }; - export const Exported = { - broadcastChangedSerialized: (_id, arg1, arg2) => instances.export(_id, /* v8 ignore next -- @preserve */ id => new JSExported(id)).broadcastChanged(instances.export(arg1, /* v8 ignore next -- @preserve */ id => new JSExported(id)), deserialize(arg2, Info)) - }; - """); - } - - [Fact] - public void DoesNotEmitDuplicateModuleRegistrations () - { - AddAssembly(With( - """ - public interface IImported - { - event Action? Changed; - event Action? Done; - } - - public class Class - { - [Export] public static void UseImported (IImported instance) {} - } - """)); - Execute(); - Once("function import_IImported"); - } - - [Fact] - public void IgnoresImplementedInterfaceMethods () - { - AddAssembly(With( - """ - [assembly:Export(typeof(IExportedStatic))] - [assembly:Import(typeof(IImportedStatic))] - - public interface IExportedStatic { int Foo () => 0; } - public interface IImportedStatic { int Foo () => 0; } - public interface IExportedInstanced { int Foo () => 0; } - public interface IImportedInstanced { int Foo () => 0; } - - public class Class - { - [Export] public static IExportedInstanced GetExported () => default; - [Import] public static IImportedInstanced GetImported () => default; - } - """)); - Execute(); - DoesNotContain("Foo"); - } -} diff --git a/src/cs/Bootsharp.Publish.Test/TaskTest.cs b/src/cs/Bootsharp.Publish.Test/TaskTest.cs index 2441db2d..75ec3f77 100644 --- a/src/cs/Bootsharp.Publish.Test/TaskTest.cs +++ b/src/cs/Bootsharp.Publish.Test/TaskTest.cs @@ -8,7 +8,8 @@ public abstract class TaskTest : IDisposable protected MockProject Project { get; } = new(); protected BuildEngine Engine { get; } = BuildEngine.Create(); protected string LastAddedAssemblyName { get; private set; } - protected virtual string TestedContent { get; } = ""; + protected virtual string TestedContent { get; set; } = ""; + protected virtual string TestedDirectory => ""; public abstract void Execute (); @@ -49,29 +50,40 @@ protected MockSource With (string code) return With(null, code); } - protected void Contains (string content) + protected void Contains (string content) => Contains(null, content); + protected void Contains (string path, string content) { - try { Assert.Contains(content, TestedContent); } - catch (Exception ex) { DumpAndThrow(ex); } + try { Assert.Contains(content, GetTestedContent(path)); } + catch (Exception ex) { DumpAndThrow(ex, GetTestedContent(path)); } } - protected void DoesNotContain (string content) + protected void DoesNotContain (string content) => DoesNotContain(null, content); + protected void DoesNotContain (string path, string content) { - try { Assert.DoesNotContain(content, TestedContent, StringComparison.OrdinalIgnoreCase); } - catch (Exception ex) { DumpAndThrow(ex); } + try { Assert.DoesNotContain(content, GetTestedContent(path), StringComparison.OrdinalIgnoreCase); } + catch (Exception ex) { DumpAndThrow(ex, GetTestedContent(path)); } } - protected MatchCollection Matches (string pattern) + protected MatchCollection Matches (string pattern) => Matches(null, pattern); + protected MatchCollection Matches (string path, string pattern) { - try { Assert.Matches(pattern, TestedContent); } - catch (Exception ex) { DumpAndThrow(ex); } - return Regex.Matches(TestedContent, pattern); + var tested = GetTestedContent(path); + try { Assert.Matches(pattern, tested); } + catch (Exception ex) { DumpAndThrow(ex, tested); } + return Regex.Matches(tested, pattern); } - protected void Once (string pattern) + protected void Once (string pattern) => Once(null, pattern); + protected void Once (string path, string pattern) { - try { Assert.Single(Matches(pattern)); } - catch (Exception ex) { DumpAndThrow(ex); } + try { Assert.Single(Matches(path, pattern)); } + catch (Exception ex) { DumpAndThrow(ex, GetTestedContent(path)); } + } + + private string GetTestedContent (string path) + { + if (path == null) return TestedContent; + return ReadProjectFile(Path.Combine(TestedDirectory, path)); } protected string ReadProjectFile (string fileName) @@ -80,10 +92,10 @@ protected string ReadProjectFile (string fileName) return File.Exists(filePath) ? File.ReadAllText(filePath) : null; } - private void DumpAndThrow (Exception ex) + private void DumpAndThrow (Exception ex, string content) { var path = Path.Combine(Project.Root, "..", "..", "..", "..", "last-failed-test-dump.txt"); - File.WriteAllText(path, TestedContent); + File.WriteAllText(path, content); throw ex; } } diff --git a/src/cs/Bootsharp.Publish/Common/CodeBuilder.cs b/src/cs/Bootsharp.Publish/Common/CodeBuilder.cs new file mode 100644 index 00000000..6e341050 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/CodeBuilder.cs @@ -0,0 +1,71 @@ +using System.Text; + +namespace Bootsharp.Publish; + +internal class CodeBuilder (string content = "") +{ + private record struct Block (string? Separator, bool Empty = true); + + private readonly StringBuilder bld = new(content); + private readonly Stack blocks = new(); + + public void Clear () + { + bld.Clear(); + blocks.Clear(); + } + + public override string ToString () + { + return bld.ToString(); + } + + public CodeBuilder Append (string content) + { + bld.Append(content); + return this; + } + + public CodeBuilder Line (string content) + { + if (blocks.TryPeek(out var blk)) + OnBlockLine(blk); + AppendLine(content); + return this; + } + + public CodeBuilder Join (string separator, IEnumerable parts) + { + bld.AppendJoin(separator, parts); + return this; + } + + public CodeBuilder Enter (string header, string? lineSeparator = null) + { + Line(header); + blocks.Push(new(lineSeparator)); + return this; + } + + public CodeBuilder Exit (string footer) + { + blocks.Pop(); + AppendLine(footer); + return this; + } + + private void OnBlockLine (Block blk) + { + if (blk is { Empty: false, Separator: { } sep }) + bld.Insert(bld.Length - 1, sep); + if (blk.Empty) + blocks.Push(blocks.Pop() with { Empty = false }); + } + + private void AppendLine (string content) + { + bld.Append(' ', blocks.Count * 4); + bld.Append(content); + bld.Append('\n'); + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 533a4d6c..54278e28 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -1,14 +1,15 @@ global using static Bootsharp.Publish.GlobalInspection; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace Bootsharp.Publish; -internal delegate TypeMeta InspectType (Type type, InteropKind ik); - internal static class GlobalInspection { + public static Preferences Pref => PreferencesResolver.Resolved.Value!; + public static MetadataLoadContext CreateLoadContext (string directory) { var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); @@ -32,15 +33,6 @@ public static bool IsUserType (Type type) return IsUserAssembly(type.Assembly.FullName!); } - public static bool IsInstancedType (Type type) - { - // Instanced types are mutable user types that are passed by reference when crossing the - // interop boundary (as opposed to serialized immutable types, which are copied by value). - if (!IsUserType(type)) return false; - if (type.IsInterface) return true; - return type.IsClass && !IsStatic(type) && !IsRecord(type); // records are immutable by convention - } - public static bool IsAutoProperty (PropertyInfo prop) { var backingFieldName = $"<{prop.Name}>k__BackingField"; @@ -49,11 +41,26 @@ public static bool IsAutoProperty (PropertyInfo prop) return backingField != null; } - public static string WithPrefs (IReadOnlyCollection prefs, string input, string @default) + public static string WithPref (IReadOnlyCollection prefs, string input, string? @default = null) { foreach (var pref in prefs) if (Regex.IsMatch(input, pref.Pattern)) return Regex.Replace(input, pref.Pattern, pref.Replacement); - return @default; + return @default ?? input; + } + + extension (IReadOnlyCollection types) + { + public bool HasBase (Type clr, [NotNullWhen(true)] out TypeMeta? bs) + { + bs = types.FirstOrDefault(t => t.Clr == clr.BaseType); + return bs != null && IsUserType(bs.Clr); + } + + public TypeMeta Get (Type clr) + { + var def = clr.IsGenericType ? clr.GetGenericTypeDefinition() : clr; + return types.First(t => (t.Clr.IsGenericType ? t.Clr.GetGenericTypeDefinition() : t.Clr) == def); + } } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs index 6dcfe512..b1986be1 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs @@ -1,4 +1,5 @@ global using static Bootsharp.Publish.GlobalText; +using System.Text; namespace Bootsharp.Publish; @@ -8,21 +9,32 @@ internal static class GlobalText public static string Fmt (int indent, params string?[] txt) => Fmt(txt, indent); public static string Fmt (IEnumerable txt, int indent = 1, string separator = "\n") { - var pad = new string(' ', indent * 4); + var pad = Pad(indent); var padded = txt.Where(v => v != null).Select(v => string.Join("\n", v!.Split('\n').Select((line, i) => i == 0 ? line : string.IsNullOrWhiteSpace(line) ? "" : pad + line))); return string.Join(separator + pad, padded); } + public static string Pad (int level) + { + return new string(' ', level * 4); + } + public static string ToFirstLower (string value) { if (value.Length == 1) return value.ToLowerInvariant(); return char.ToLowerInvariant(value[0]) + value[1..]; } - public static string IgnoreV8 (this string content, string before) + public static string Slugify (string value) { - return content.Replace(before, $"/* v8 ignore next -- @preserve */ {before}"); + var bld = new StringBuilder(value.Length + 4); + for (var i = 0; i < value.Length; i++) + if (value[i] == '.') bld.Append('/'); + else if (char.IsUpper(value[i]) && i > 0 && char.IsLower(value[i - 1])) + bld.Append('-').Append(char.ToLowerInvariant(value[i])); + else bld.Append(char.ToLowerInvariant(value[i])); + return bld.ToString(); } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 9555ce80..80394ed0 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; namespace Bootsharp.Publish; @@ -93,17 +94,6 @@ public static bool IsNullable (Type type, NullabilityInfo? info, [NotNullWhen(tr return value != null; } - public static string BuildJSSpace (Type type, Preferences prefs) - { - var space = type.Namespace ?? ""; - if (type.IsNested) - { - if (!string.IsNullOrEmpty(space)) space += "."; - space += type.DeclaringType!.Name; - } - return WithPrefs(prefs.Space, space, space); - } - public static string BuildJSName (string name) { name = ToFirstLower(name); @@ -116,7 +106,7 @@ public static string PrependIdArg (string args) return $"_id, {args}"; } - public static string BuildSerializedId (Type type) + public static string BuildId (Type type) { var builder = new StringBuilder(); foreach (var c in BuildSyntax(type).Replace("global::", "")) @@ -155,18 +145,14 @@ static string ResolveTypeName (Type type) public static string TrimGeneric (string typeName) { - var delimiterIndex = typeName.IndexOf('`'); - if (delimiterIndex < 0) return typeName; - return typeName[..delimiterIndex]; + return Regex.Replace(typeName, @"`\d+(\[\[.*\]\])?", ""); } public static string Export (ArgumentMeta arg) => Export(arg.Value, arg.Name); public static string Export (ValueMeta value, string exp) => Export(value.Type, exp); public static string Export (TypeMeta type, string exp) { - if (type is InstancedMeta it) - if (it.Interop == InteropKind.Export) return $"Instances.Export({exp})"; - else return $"((global::{it.FullName}){exp})._id"; + if (type is InstanceMeta) return $"Instances.Export({exp})"; if (type is SerializedMeta sm) return $"Serializer.Serialize({exp}, SerializerContext.{sm.Id})"; return exp; } @@ -175,9 +161,7 @@ public static string Export (TypeMeta type, string exp) public static string Import (ValueMeta value, string exp) => Import(value.Type, exp); public static string Import (TypeMeta type, string exp) { - if (type is InstancedMeta it) - if (it.Interop == InteropKind.Export) return $"Instances.Exported<{it.Syntax}>({exp})"; - else return $"Instances.Import({exp}, static id => new global::{it.FullName}(id))"; + if (type is InstanceMeta it) return $"Instances.Resolve<{it.Syntax}>({exp})"; if (type is SerializedMeta sm) return $"Serializer.Deserialize({exp}, SerializerContext.{sm.Id})"; return exp; } @@ -186,11 +170,8 @@ public static string Import (TypeMeta type, string exp) public static string ExportJS (ValueMeta value, string exp) => ExportJS(value.Type, exp); public static string ExportJS (TypeMeta type, string exp) { - if (type is InstancedMeta it) - if (it.Interop == InteropKind.Export) return $"{exp}._id"; - else if (it.Importer is { } importer) return $"{importer}({exp})"; - else return $"instances.import({exp})"; - if (type is SerializedMeta sm) return $"serialize({exp}, {sm.Id})"; + if (type is InstanceMeta it) return $"$i.resolve({exp}, $i.{it.Id})"; + if (type is SerializedMeta sm) return $"deserialize({exp}, $s.{sm.Id})"; return exp; } @@ -198,10 +179,10 @@ public static string ExportJS (TypeMeta type, string exp) public static string ImportJS (ValueMeta value, string exp) => ImportJS(value.Type, exp); public static string ImportJS (TypeMeta type, string exp) { - if (type is InstancedMeta it) - if (it.Interop == InteropKind.Import) return $"instances.imported({exp})"; - else return $"instances.export({exp}, id => new {it.JSName}(id))"; - if (type is SerializedMeta sm) return $"deserialize({exp}, {sm.Id})"; + if (type is InstanceMeta it) + if (it.Importer is { } importer) return $"$i.{importer}({exp})"; + else return $"$i.import({exp})"; + if (type is SerializedMeta sm) return $"serialize({exp}, $s.{sm.Id})"; return exp; } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/Inspection/InspectionReporter.cs similarity index 77% rename from src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/InspectionReporter.cs index d0161577..54b8d86b 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/InspectionReporter.cs @@ -18,18 +18,14 @@ public void Report (SolutionInspection spec) private HashSet GetDiscoveredAssemblies (SolutionInspection spec) { - return spec.Static - .Concat(spec.Modules.SelectMany(i => i.Members)) - .Concat(spec.Instanced.SelectMany(i => i.Members)) + return spec.Types.OfType().SelectMany(s => s.Members) .Select(m => m.Info.DeclaringType!.Assembly.GetName().Name!) .ToHashSet(); } private HashSet GetDiscoveredMembers (SolutionInspection spec) { - return spec.Static - .Concat(spec.Modules.SelectMany(i => i.Members)) - .Concat(spec.Instanced.SelectMany(i => i.Members)) + return spec.Types.OfType().SelectMany(s => s.Members) .Select(m => m.ToString()) .ToHashSet(); } diff --git a/src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/DocMeta.cs similarity index 79% rename from src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/Meta/DocMeta.cs index 0229bec0..37c7945c 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/DocMeta.cs @@ -7,4 +7,4 @@ namespace Bootsharp.Publish; /// /// Name of the assembly associated with the documentation. /// The XML documentation. -internal sealed record DocumentationMeta (string Assembly, XDocument Xml); +internal sealed record DocMeta (string Assembly, XDocument Xml); diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/InteropKind.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/Meta/InteropKind.cs diff --git a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs similarity index 68% rename from src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs index 07404c53..aaf5fbe3 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs @@ -4,7 +4,7 @@ namespace Bootsharp.Publish; /// -/// An interop member declared on a static, module or instanced API surface. +/// Describes a member declared on an interop surface (). /// internal abstract record MemberMeta { @@ -13,19 +13,14 @@ internal abstract record MemberMeta /// public abstract MemberInfo Info { get; } /// - /// Whether the member is implemented in C# and exposed to JavaScript (export) - /// or implemented in JavaScript and consumed from C# (import). + /// Describes the interop surface on which the member is declared. /// - public required InteropKind Interop { get; init; } + public required SurfaceMeta Surf { get; init; } /// - /// Full name of the C# type (including namespace), under which the member is declared. - /// - public required string Space { get; init; } - /// - /// JavaScript object name(s) (joined with dot when nested) under which the associated interop - /// member will be declared; resolved from with user-defined converters. + /// Whether the member is implemented in C# and exposed to JavaScript (export) + /// or implemented in JavaScript and consumed from C# (import). /// - public required string JSSpace { get; init; } + public required InteropKind IK { get; init; } /// /// C# name of the member, as specified in source code or generated for the interop implementation. /// @@ -37,7 +32,7 @@ internal abstract record MemberMeta } /// -/// An interop method declared on a static, module or instanced API surface. +/// Describes a method declared on an interop surface. /// internal record MethodMeta (MethodInfo Info) : MemberMeta { @@ -48,7 +43,7 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta /// /// Arguments of the method. /// - public required IReadOnlyList Arguments { get; init; } + public required IReadOnlyList Args { get; init; } /// /// Method's return value. /// @@ -64,7 +59,7 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta } /// -/// An interop event declared on a static, module or instanced API surface. +/// Describes an event declared on an interop surface. /// internal sealed record EventMeta (EventInfo Info) : MemberMeta { @@ -73,13 +68,17 @@ internal sealed record EventMeta (EventInfo Info) : MemberMeta /// public override EventInfo Info { get; } = Info; /// + /// Fully qualified C# syntax of the event type, including nullable annotations. + /// + public required string TypeSyntax { get; init; } + /// /// Arguments carried by the event delegate. /// - public required IReadOnlyList Arguments { get; init; } + public required IReadOnlyList Args { get; init; } } /// -/// An interop property declared on a static, module or instanced API surface. +/// Describes a property declared on an interop surface. /// internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta { @@ -88,27 +87,31 @@ internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta /// public override PropertyInfo Info { get; } = Info; /// - /// Get value of the property or null when getter is not accessible. + /// Fully qualified C# syntax of the property type, including nullable annotations. + /// + public required string TypeSyntax { get; init; } + /// + /// Describes the getter value of the property. /// - public required ValueMeta? GetValue { get; init; } + public required ValueMeta? Get { get; init; } /// - /// Set value of the property or null when setter is not accessible. + /// Describes the setter value of the property. /// - public required ValueMeta? SetValue { get; init; } + public required ValueMeta? Set { get; init; } /// /// Whether the property has an accessible getter. /// - [MemberNotNullWhen(true, nameof(GetValue))] - public bool CanGet => GetValue != null; + [MemberNotNullWhen(true, nameof(Get))] + public bool CanGet => Get != null; /// /// Whether the property has an accessible setter. /// - [MemberNotNullWhen(true, nameof(SetValue))] - public bool CanSet => SetValue != null; + [MemberNotNullWhen(true, nameof(Set))] + public bool CanSet => Set != null; } /// -/// An interop method or event delegate argument. +/// Describes a method or event delegate argument. /// internal sealed record ArgumentMeta (ParameterInfo Info) { diff --git a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SerializedMeta.cs similarity index 88% rename from src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/Meta/SerializedMeta.cs index 594b11c9..0a9317e3 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SerializedMeta.cs @@ -5,13 +5,7 @@ namespace Bootsharp.Publish; /// /// Describes an immutable CLR type that is serialized and copied by value when crossing the interop boundary. /// -internal abstract record SerializedMeta (Type Clr) : TypeMeta(Clr) -{ - /// - /// The identifier of the serializer factory associated with the type. - /// - public string Id { get; } = BuildSerializedId(Clr); -} +internal abstract record SerializedMeta (Type Clr) : TypeMeta(Clr); /// /// Describes a serialized primitive (string, int, bool, etc). @@ -53,7 +47,7 @@ internal sealed record SerializedDictionaryMeta (Type Clr, /// Describes an instance reference under a serialized object. /// /// The associated instanced type info. -internal sealed record SerializedInstanceMeta (InstancedMeta Instance) : SerializedMeta(Instance.Clr); +internal sealed record SerializedInstanceMeta (InstanceMeta Instance) : SerializedMeta(Instance.Clr); /// /// Describes a serialized user-defined object, such as a record or a struct. @@ -65,13 +59,17 @@ internal sealed record SerializedObjectMeta (Type Clr, /// /// Describes a serializable property of a . /// -internal sealed record SerializedPropertyMeta (Type Clr) : SerializedMeta(Clr) +internal sealed record SerializedPropertyMeta { /// /// The reflected info of the property. /// public required PropertyInfo Info { get; init; } /// + /// Describes the type of the property. + /// + public required SerializedMeta Type { get; init; } + /// /// The name of the property. /// public required string Name { get; init; } @@ -86,11 +84,11 @@ internal sealed record SerializedPropertyMeta (Type Clr) : SerializedMeta(Clr) /// /// Whether the property should be omitted from serialization output when null. /// - public required bool OmitWhenNull { get; init; } + public required bool Nullable { get; init; } /// /// Whether the property is bound to a constructor parameter. /// - public required bool ConstructorParameter { get; init; } + public required bool Ctor { get; init; } /// /// How the property can be assigned during deserialization. /// @@ -98,7 +96,7 @@ internal sealed record SerializedPropertyMeta (Type Clr) : SerializedMeta(Clr) /// /// Name of the generated unsafe field accessor method when . /// - public required string? FieldAccessorName { get; init; } + public required string? FieldAccessor { get; init; } } /// diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs new file mode 100644 index 00000000..8bf23dfe --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/SurfaceMeta.cs @@ -0,0 +1,87 @@ +namespace Bootsharp.Publish; + +/// +/// Describes a CLR type that projects an interop API surface. +/// +internal abstract record SurfaceMeta (Type Clr) : TypeMeta(Clr) +{ + /// + /// Interop API members declared on the surface. + /// + public required IReadOnlyCollection Members { get; init; } +} + +/// +/// Describes an interop surface encompassing static interop members specified via class-level +/// or attributes. +/// +internal record StaticMeta (Type Clr) : SurfaceMeta(Clr); + +/// +/// Describes an interop surface that uses a generated proxy to bind with the source. +/// +internal abstract record ProxyMeta (Type Clr) : SurfaceMeta(Clr) +{ + /// + /// Whether the proxy is exported from C# or imported from JavaScript. + /// + public required InteropKind IK { get; init; } + /// + /// Describes the generated proxy. + /// + public required SurfaceProxy Proxy { get; init; } +} + +/// +/// Describes an interop surface specified via assembly-level or +/// attributes. +/// +internal record ModuleMeta (Type Clr) : ProxyMeta(Clr); + +/// +/// Describes an interop surface projected from an instanced type. +/// Instanced are mutable types whose instances are passed by reference when crossing the interop boundary. +/// +/// +/// Note that 2 instance surfaces are possible for a single instanced type in cases when the type participates +/// in both exported and imported interop directions; for example, an interface get+set property may get +/// instances implemented in JavaScript and set instances of the same interface, but implemented in C#. +/// +internal record InstanceMeta (Type Clr) : ProxyMeta(Clr) +{ + /// + /// Name of the specialized C# exporter method or null when not required. + /// + public string? Exporter { get; init; } + /// + /// Name of the specialized JS importer function or null when not required. + /// + public string? Importer { get; init; } +} + +/// +/// Describes the generated proxy used by . +/// +public record SurfaceProxy +{ + /// + /// Unique identifier of the generated C# proxy type. + /// + 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 new file mode 100644 index 00000000..d84ebba7 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/TypeMeta.cs @@ -0,0 +1,29 @@ +namespace Bootsharp.Publish; + +/// +/// Describes a C# CLR type that either crosses the interop boundary directly, or is otherwise associated +/// with such types; the derivatives are and . +/// +internal record TypeMeta (Type Clr) +{ + /// + /// The described CLR type. + /// + public Type Clr { get; } = Clr; + /// + /// Unique identifier of the type. + /// + public string Id { get; } = BuildId(Clr); + /// + /// Fully qualified C# syntax of the type. + /// + public string Syntax { get; } = BuildSyntax(Clr); + /// + /// The path to the module containing the node that represents the type in JavaScript. + /// + public string JSModule { get; } = Slugify(WithPref(Pref.Space, Clr.Namespace ?? "index")); + /// + /// The path to the node inside the module that represents the type in JavaScript. + /// + public string JSNode { get; } = WithPref(Pref.Name, TrimGeneric(Clr.FullName!).Split('.')[^1]).Replace('+', '.'); +} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/ValueMeta.cs similarity index 96% rename from src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/Meta/ValueMeta.cs index c02b1433..65ecbf7d 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/ValueMeta.cs @@ -27,7 +27,7 @@ internal sealed record ValueMeta /// /// Instance info when , null otherwise. /// - public InstancedMeta? Instanced => Type as InstancedMeta; + public InstanceMeta? Instanced => Type as InstanceMeta; /// /// Whether the value is serialized and copied when crossing the interop boundary. /// diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs similarity index 89% rename from src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs index 39a4b2fa..dbf4d327 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs @@ -9,7 +9,7 @@ namespace Bootsharp.Publish; /// and whose types are not natively supported by System.Runtime.InteropServices.JavaScript. /// The types that are referenced by these top-level interop types are crawled by this inspector. /// -internal sealed class SerializedInspector (InstancedInspector itd) +internal sealed class SerializedInspector (TypeInspector.InspectInstanced inspectInstanced) { private record Discard (Type Type) : SerializedMeta(Type); @@ -29,7 +29,7 @@ private record Discard (Type Type) : SerializedMeta(Type); public SerializedMeta? Inspect (Type type, InteropKind ik) { this.ik = ik; - return ShouldSerialize(type) ? Build(type) : null; + return IsSerialized(type) ? Build(type) : null; } public IReadOnlyList Collect () @@ -37,18 +37,18 @@ public IReadOnlyList Collect () return OrderByDependencyGraph(byId.Values); // initialization order matters for JavaScript } - private static bool ShouldSerialize (Type type) + private static bool IsSerialized (Type type) { if (IsVoid(type)) return false; - if (IsNullable(type, out var value)) return ShouldSerialize(value); - if (IsTaskWithResult(type, out var result)) return ShouldSerialize(result); + if (IsNullable(type, out var value)) return IsSerialized(value); + if (IsTaskWithResult(type, out var result)) return IsSerialized(result); return !native.Contains(type.FullName!); } private SerializedMeta Build (Type type) { if (IsTaskWithResult(type, out var result)) type = result; // tasks are natively supported - ignore them - var id = BuildSerializedId(type); + var id = BuildId(type); if (byId.TryGetValue(id, out var existing)) return existing; if (!cycle.Add(type)) return new Discard(type); // break self-ref cycle var meta = byId[id] = @@ -58,7 +58,7 @@ private SerializedMeta Build (Type type) type.IsArray ? new SerializedArrayMeta(type, Build(type.GetElementType()!)) : IsList(type, out var element) ? new SerializedListMeta(type, Build(element)) : IsDictionary(type, out var k, out var v) ? new SerializedDictionaryMeta(type, Build(k), Build(v)) : - IsInstancedType(type) ? new SerializedInstanceMeta(itd.Inspect(type, ik)!) : + inspectInstanced(type, ik) is { } it ? new SerializedInstanceMeta(it) : BuildObject(type); cycle.Remove(type); return meta; @@ -86,21 +86,21 @@ private SerializedObjectMeta BuildObject (Type type) private SerializedPropertyMeta BuildProperty (PropertyInfo prop, bool ctor) { - var value = Build(prop.PropertyType); var setter = prop.SetMethod is { IsPublic: true } ? prop.SetMethod : null; var initOnly = setter?.ReturnParameter.GetRequiredCustomModifiers() .Any(m => m.FullName == typeof(IsExternalInit).FullName) == true; - return new(value.Clr) { + return new() { Info = prop, + Type = Build(prop.PropertyType), Name = prop.Name, JSName = BuildJSName(prop.Name), - OmitWhenNull = IsNullable(prop.PropertyType, GetNullity(prop)), + Nullable = IsNullable(prop.PropertyType, GetNullity(prop)), Required = prop.CustomAttributes .Any(a => a.AttributeType.FullName == typeof(RequiredMemberAttribute).FullName), - ConstructorParameter = ctor, + Ctor = ctor, Kind = setter == null ? SerializedPropertyKind.Field : initOnly ? SerializedPropertyKind.Init : SerializedPropertyKind.Set, - FieldAccessorName = setter == null ? $"Access_{BuildSerializedId(prop.DeclaringType!)}_{prop.Name}" : null + FieldAccessor = setter == null ? $"Access_{BuildId(prop.DeclaringType!)}_{prop.Name}" : null }; } @@ -130,8 +130,8 @@ private static IReadOnlyList OrderByDependencyGraph (IEnumerable var metas = types.DistinctBy(m => m.Id).ToDictionary(m => m.Id); var pending = metas.ToDictionary( m => m.Key, - m => GetInitDependencies(m.Value).Where(metas.ContainsKey).ToHashSet(StringComparer.Ordinal)); - var dependents = metas.Keys.ToDictionary(k => k, _ => new List(), StringComparer.Ordinal); + m => GetInitDependencies(m.Value).Where(metas.ContainsKey).ToHashSet()); + var dependents = metas.Keys.ToDictionary(k => k, _ => new List()); foreach (var (id, dependencies) in pending) foreach (var dependency in dependencies) dependents[dependency].Add(id); diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspection.cs new file mode 100644 index 00000000..f1f224fe --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspection.cs @@ -0,0 +1,30 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +/// +/// Metadata about the built C# solution required to generate interop +/// code and other Bootsharp-specific resources. +/// +/// +/// Context in which the solution's assemblies were loaded and inspected. +/// Shouldn't be disposed to keep C# reflection APIs usable on the inspected types. +/// Dispose to remove file lock on the inspected assemblies. +/// +internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable +{ + /// + /// The discovered interop artifacts. + /// + public required IReadOnlyCollection Types { get; init; } + /// + /// C# XML documentation for the inspected assemblies. + /// + public required IReadOnlyCollection Docs { get; init; } + /// + /// Warnings logged while inspecting the solution. + /// + public required IReadOnlyCollection Warnings { get; init; } + + public void Dispose () => ctx.Dispose(); +} diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspector.cs new file mode 100644 index 00000000..a1e1c1ac --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/SolutionInspector.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Xml.Linq; + +namespace Bootsharp.Publish; + +internal sealed class SolutionInspector +{ + private readonly TypeInspector types = new(); + private readonly List docs = []; + private readonly List warns = []; + + /// + /// Inspects specified solution assembly paths in the output directory. + /// + /// Absolute path to directory containing compiled assemblies. + /// Absolute paths of the assemblies to inspect. + public SolutionInspection Inspect (string directory, IEnumerable paths) + { + var ctx = CreateLoadContext(directory); + foreach (var assemblyPath in paths) + try { InspectAssembly(assemblyPath, ctx); } + catch (Exception e) { AddSkippedWarning(assemblyPath, e); } + return new(ctx) { + Types = types.Collect(), + Docs = docs.ToArray(), + Warnings = warns.ToArray() + }; + } + + private void InspectAssembly (string assemblyPath, MetadataLoadContext ctx) + { + var name = Path.GetFileNameWithoutExtension(assemblyPath); + if (!IsUserAssembly(name)) return; + types.Inspect(ctx.LoadFromAssemblyPath(assemblyPath)); + InspectDocs(assemblyPath, name); + } + + private void AddSkippedWarning (string assemblyPath, Exception exception) + { + var fileName = Path.GetFileName(assemblyPath); + var message = $"Failed to inspect '{fileName}' assembly; " + + $"affected interop members won't be available in JavaScript. Error: {exception.Message}"; + warns.Add(message); + } + + private void InspectDocs (string assemblyPath, string assemblyName) + { + var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); + if (File.Exists(xmlPath)) docs.Add(new(assemblyName, XDocument.Load(xmlPath))); + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs new file mode 100644 index 00000000..1b702212 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -0,0 +1,201 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +internal sealed class TypeInspector +{ + internal delegate InstanceMeta? InspectInstanced (Type type, InteropKind ik); + + private readonly Dictionary<(Type, InteropKind), InstanceMeta> its = []; + private readonly Dictionary crawled = []; + private readonly HashSet inspectedModuleTypes = []; + private readonly List surfaces = []; + private readonly SerializedInspector srd; + + public TypeInspector () + { + srd = new SerializedInspector(InspectInstance); + } + + public void Inspect (Assembly assembly) + { + foreach (var type in assembly.GetExportedTypes()) + if (InspectStatic(type) is { } st) + surfaces.Add(st); + foreach (var attr in assembly.CustomAttributes) + if (ResolveIK(attr) is { } ik) + foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) + if (InspectModule((Type)arg.Value!, ik) is { } md) + surfaces.Add(md); + } + + public IReadOnlyCollection Collect () + { + TypeMeta[] specialized = [..surfaces, ..its.Values, ..srd.Collect()]; + var clrs = specialized.Select(t => t.Clr).ToHashSet(); + return [..specialized, ..crawled.Values.Where(c => !clrs.Contains(c.Clr))]; + } + + private StaticMeta? InspectStatic (Type type) + { + if (type.Namespace?.StartsWith("Bootsharp.Generated") == true) return null; + var members = new List(); + var st = new StaticMeta(type) { Members = members }; + var flags = BindingFlags.Public | BindingFlags.Static; + foreach (var evt in type.GetEvents(flags)) + if (ResolveIK(evt) is { } ik) + members.Add(InspectEvent(evt, ik, st)); + foreach (var prop in type.GetProperties(flags)) + if (ResolveIK(prop) is { } ik) + members.Add(InspectProperty(prop, ik, st)); + foreach (var method in type.GetMethods(flags)) + if (ResolveIK(method) is { } ik) + members.Add(InspectMethod(method, ik, st)); + return members.Count > 0 ? st : null; + } + + private ModuleMeta? InspectModule (Type type, InteropKind ik) + { + if (!inspectedModuleTypes.Add(type) || IsStatic(type) || + ik == InteropKind.Import && !type.IsInterface) return null; + var md = new ModuleMeta(type) { + IK = ik, + Proxy = BuildProxy(type, ik), + Members = new List() + }; + return InspectMembers(md, ik); + } + + private InstanceMeta? InspectInstance (Type type, InteropKind ik) + { + if (its.TryGetValue((type, ik), out var it)) return it; + if (IsTaskWithResult(type, out var result)) return InspectInstance(result, ik); + if (!IsInstanced(type)) return null; + // instances with events need specialized registrars to un-/sub them + var special = type.GetEvents().Length > 0; + it = its[(type, ik)] = new(type) { + IK = ik, + Proxy = BuildProxy(type, ik), + Members = new List(), + Exporter = special && ik == InteropKind.Export ? "Export" : null, // discriminated by types on C# + Importer = special && ik == InteropKind.Import ? $"import_{BuildId(type)}" : null, + }; + return InspectMembers(it, ik); + + static bool IsInstanced (Type type) + { + // Instanced types are mutable user types that are passed by reference when crossing the + // interop boundary (as opposed to serialized immutable types, which are copied by value). + if (!IsUserType(type)) return false; + if (type.IsInterface) return true; + return type.IsClass && !IsStatic(type) && !IsRecord(type); // records are immutable by convention + } + } + + private T InspectMembers (T surf, InteropKind ik) where T : SurfaceMeta + { + var members = (ICollection)surf.Members; + foreach (var evt in surf.Clr.GetEvents()) + members.Add(InspectEvent(evt, ik, surf)); + foreach (var prop in surf.Clr.GetProperties()) + if (ShouldInspectProperty(prop)) + members.Add(InspectProperty(prop, ik, surf)); + foreach (var method in surf.Clr.GetMethods()) + if (ShouldInspectMethod(method)) + members.Add(InspectMethod(method, ik, surf)); + return surf; + + static bool ShouldInspectProperty (PropertyInfo prop) + { + if (prop.GetIndexParameters().Length != 0) return false; + if (prop.DeclaringType!.IsInterface) + return prop.GetMethod?.IsAbstract == true || + prop.SetMethod?.IsAbstract == true; + return true; + } + + static bool ShouldInspectMethod (MethodInfo method) + { + if (method.IsSpecialName) return false; + if (method.DeclaringType!.FullName == typeof(object).FullName) return false; + if (method.DeclaringType!.IsInterface) return method.IsAbstract; + return !method.IsStatic; + } + } + + private EventMeta InspectEvent (EventInfo evt, InteropKind ik, SurfaceMeta srf) => new(evt) { + IK = ik, + Surf = srf, + Name = evt.Name, + JSName = WithPref(Pref.Event, evt.Name, BuildJSName(evt.Name)), + TypeSyntax = BuildSyntax(evt.EventHandlerType!, GetNullity(evt)), + Args = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() + .Select(p => InspectArg(p, GetNullity(evt, p), ik)).ToArray() + }; + + private PropertyMeta InspectProperty (PropertyInfo prop, InteropKind ik, SurfaceMeta srf) => new(prop) { + IK = ik, + Surf = srf, + Name = 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, + Set = prop.SetMethod != null ? InspectValue(prop.PropertyType, GetNullity(prop), ik.Invert) : null + }; + + private MethodMeta InspectMethod (MethodInfo method, InteropKind ik, SurfaceMeta srf) => new(method) { + IK = ik, + Surf = srf, + Name = 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), + Void = IsVoid(method.ReturnParameter.ParameterType), + Async = IsTaskLike(method.ReturnParameter.ParameterType) + }; + + private ArgumentMeta InspectArg (ParameterInfo param, NullabilityInfo nil, InteropKind ik) => new(param) { + Name = param.Name!, + JSName = BuildJSName(param.Name!), + Value = InspectValue(param.ParameterType, nil, ik) + }; + + private ValueMeta InspectValue (Type type, NullabilityInfo nil, InteropKind ik) => new() { + Type = InspectType(type, ik), + TypeSyntax = BuildSyntax(type, nil), + Nullable = IsNullable(type, nil) + }; + + private TypeMeta InspectType (Type type, InteropKind ik) + { + for (var clr = type; clr.IsNested && IsUserType(clr.DeclaringType!); clr = clr.DeclaringType!) + crawled.TryAdd(clr.DeclaringType!, new(clr.DeclaringType!)); + return InspectInstance(type, ik) ?? srd.Inspect(type, ik) ?? new TypeMeta(type); + } + + 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 }; + } + + private InteropKind? ResolveIK (MemberInfo info) + { + foreach (var attr in info.CustomAttributes) + if (ResolveIK(attr) is { } ik) + return ik; + return null; + } + + private InteropKind? ResolveIK (CustomAttributeData attr) + { + if (attr.AttributeType.FullName == typeof(ExportAttribute).FullName) return InteropKind.Export; + if (attr.AttributeType.FullName == typeof(ImportAttribute).FullName) return InteropKind.Import; + return null; + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs deleted file mode 100644 index d29202a1..00000000 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Reflection; - -namespace Bootsharp.Publish; - -internal sealed class InstancedInspector (MemberInspector members) -{ - private readonly Dictionary byType = []; - private readonly HashSet modules = []; - - public InstancedMeta? Inspect (Type type, InteropKind ik) - { - if (byType.TryGetValue(type, out var meta)) return meta; - if (IsTaskWithResult(type, out var result)) return Inspect(result, ik); - if (!IsInstancedType(type)) return null; - return CollectMembers(byType[type] = InspectType(type, ik)); - } - - public ModuleMeta? InspectModule (Type type, InteropKind ik) - { - if (!modules.Add(type)) return null; - if (ik == InteropKind.Import && !type.IsInterface) return null; - if (IsStatic(type)) return null; - var it = CollectMembers(InspectType(type, ik)); - return new(type) { Interop = ik, Namespace = it.Namespace, Name = it.Name, Members = it.Members }; - } - - public IReadOnlyCollection Collect () - { - return byType.Values.ToArray(); - } - - private InstancedMeta InspectType (Type type, InteropKind ik) => new(type) { - Interop = ik, - Namespace = BuildSpace(type, ik), - Name = BuildName(type), - JSName = BuildJSName(type), - Members = new List(), - Exporter = BuildExporter(type, ik), - Importer = BuildImporter(type, ik) - }; - - private InstancedMeta CollectMembers (InstancedMeta it) - { - var cl = (List)it.Members; - cl.AddRange(it.Clr.GetEvents().Select(m => members.Inspect(m, it.Interop))); - cl.AddRange(it.Clr.GetProperties().Where(ShouldInspectProperty).Select(m => members.Inspect(m, it.Interop))); - cl.AddRange(it.Clr.GetMethods().Where(ShouldInspectMethod).Select(m => members.Inspect(m, it.Interop))); - return it; - } - - private bool ShouldInspectProperty (PropertyInfo prop) - { - if (prop.GetIndexParameters().Length != 0) return false; - if (prop.DeclaringType!.IsInterface) - return prop.GetMethod?.IsAbstract == true || - prop.SetMethod?.IsAbstract == true; - return true; - } - - private bool ShouldInspectMethod (MethodInfo method) - { - if (method.IsSpecialName) return false; - if (method.DeclaringType!.FullName == typeof(object).FullName) return false; - if (method.DeclaringType!.IsInterface) return method.IsAbstract; - return !method.IsStatic; - } - - private string BuildSpace (Type type, InteropKind ik) - { - var space = "Bootsharp.Generated." + (ik == InteropKind.Export ? "Exports" : "Imports"); - if (type.Namespace != null) space += $".{type.Namespace}"; - return space; - } - - private string BuildName (Type type) - { - var trimmed = type.IsInterface ? type.Name[1..] : type.Name; - return "JS" + trimmed; - } - - private string BuildJSName (Type type) - { - var name = BuildName(type); - if (type.Namespace == null) return name; - return $"{type.Namespace}.{name}".Replace(".", "_"); - } - - private string? BuildExporter (Type type, InteropKind ik) - { - if (ik != InteropKind.Export || type.GetEvents().Length == 0) return null; - return "Export"; // we're using method overloads instead of unique names - } - - private string? BuildImporter (Type type, InteropKind ik) - { - if (ik != InteropKind.Import || type.GetEvents().Length == 0) return null; - return $"import_{type.FullName!.Replace('.', '_').Replace('+', '_')}"; - } -} diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs deleted file mode 100644 index 52e5b76a..00000000 --- a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Reflection; - -namespace Bootsharp.Publish; - -internal sealed class MemberInspector (Preferences prefs, InspectType inspect) -{ - public EventMeta Inspect (EventInfo evt, InteropKind ik) => new(evt) { - Interop = ik, - Space = evt.DeclaringType!.FullName!, - JSSpace = BuildJSSpace(evt.DeclaringType!), - Name = evt.Name, - JSName = BuildJSName(evt.Name), - Arguments = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() - .Select(p => CreateArg(p, GetNullity(evt, p), ik)).ToArray() - }; - - public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik) => new(prop) { - Interop = ik, - Space = prop.DeclaringType!.FullName!, - JSSpace = BuildJSSpace(prop.DeclaringType!), - Name = prop.Name, - JSName = BuildJSName(prop.Name), - GetValue = prop.GetMethod != null ? CreateValue(prop.PropertyType, GetNullity(prop), ik) : null, - SetValue = prop.SetMethod != null ? CreateValue(prop.PropertyType, GetNullity(prop), ik.Invert) : null - }; - - public MethodMeta Inspect (MethodInfo method, InteropKind ik) => new(method) { - Interop = ik, - Space = method.DeclaringType!.FullName!, - JSSpace = BuildJSSpace(method.DeclaringType!), - Name = method.Name, - JSName = WithPrefs(prefs.Function, method.Name, BuildJSName(method.Name)), - Arguments = method.GetParameters().Select(p => CreateArg(p, GetNullity(p), ik.Invert)).ToArray(), - Return = CreateValue(method.ReturnParameter.ParameterType, GetNullity(method.ReturnParameter), ik), - Void = IsVoid(method.ReturnParameter.ParameterType), - Async = IsTaskLike(method.ReturnParameter.ParameterType) - }; - - private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil, InteropKind ik) => new(param) { - Name = param.Name!, - JSName = BuildJSName(param.Name!), - Value = CreateValue(param.ParameterType, nil, ik) - }; - - private ValueMeta CreateValue (Type type, NullabilityInfo nil, InteropKind ik) => new() { - Type = inspect(type, ik), - TypeSyntax = BuildSyntax(type, nil), - Nullable = IsNullable(type, nil) - }; - - private string BuildJSSpace (Type decl) - { - var space = decl.Namespace ?? ""; - var name = TrimGeneric(decl.Name); - if (decl.IsInterface) name = name[1..]; - var fullname = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; - return WithPrefs(prefs.Space, fullname, fullname); - } -} diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs deleted file mode 100644 index 64b04456..00000000 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Reflection; - -namespace Bootsharp.Publish; - -/// -/// Metadata about the built C# solution required to generate interop -/// code and other Bootsharp-specific resources. -/// -/// -/// Context in which the solution's assemblies were loaded and inspected. -/// Shouldn't be disposed to keep C# reflection APIs usable on the inspected types. -/// Dispose to remove file lock on the inspected assemblies. -/// -internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable -{ - /// - /// Individual interop members, ie methods, properties or events with - /// or found on user-defined static classes. - /// - public required IReadOnlyCollection Static { get; init; } - /// - /// Interop API surfaces specified under assembly-level - /// or attributes. - /// - public required IReadOnlyCollection Modules { get; init; } - /// - /// All the immutable types that are serialized and copied by value when crossing the interop boundary. - /// - public required IReadOnlyCollection Serialized { get; init; } - /// - /// All the mutable types whose instances are passed by reference when crossing the interop boundary. - /// - public required IReadOnlyCollection Instanced { get; init; } - /// - /// C# XML documentation for the inspected assemblies. - /// - public required IReadOnlyCollection Documentation { get; init; } - /// - /// Warnings logged while inspecting the solution. - /// - public required IReadOnlyCollection Warnings { get; init; } - - public void Dispose () => ctx.Dispose(); -} diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs deleted file mode 100644 index 6a0892d3..00000000 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Reflection; -using System.Xml.Linq; - -namespace Bootsharp.Publish; - -internal sealed class SolutionInspector -{ - private readonly List statics = []; - private readonly List modules = []; - private readonly List docs = []; - private readonly List warnings = []; - private readonly MemberInspector members; - private readonly InstancedInspector itd; - private readonly SerializedInspector serde; - - public SolutionInspector (Preferences prefs) - { - members = new(prefs, InspectType); - itd = new(members); - serde = new(itd); - } - - /// - /// Inspects specified solution assembly paths in the output directory. - /// - /// Absolute path to directory containing compiled assemblies. - /// Absolute paths of the assemblies to inspect. - public SolutionInspection Inspect (string directory, IEnumerable paths) - { - var ctx = CreateLoadContext(directory); - foreach (var assemblyPath in paths) - try { InspectAssemblyFile(assemblyPath, ctx); } - catch (Exception e) { AddSkippedAssemblyWarning(assemblyPath, e); } - return CreateInspection(ctx); - } - - private TypeMeta InspectType (Type type, InteropKind ik) - { - return itd.Inspect(type, ik) ?? serde.Inspect(type, ik) ?? new TypeMeta(type); - } - - private void InspectAssemblyFile (string assemblyPath, MetadataLoadContext ctx) - { - var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); - if (!IsUserAssembly(assemblyName)) return; - InspectDocumentation(assemblyPath, assemblyName); - InspectAssembly(ctx.LoadFromAssemblyPath(assemblyPath)); - } - - private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception) - { - var fileName = Path.GetFileName(assemblyPath); - var message = $"Failed to inspect '{fileName}' assembly; " + - $"affected interop members won't be available in JavaScript. Error: {exception.Message}"; - warnings.Add(message); - } - - private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { - Static = statics.ToArray(), - Modules = modules.ToArray(), - Instanced = itd.Collect(), - Serialized = serde.Collect(), - Documentation = docs.ToArray(), - Warnings = warnings.ToArray() - }; - - private void InspectDocumentation (string assemblyPath, string assemblyName) - { - var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); - if (File.Exists(xmlPath)) docs.Add(new(assemblyName, XDocument.Load(xmlPath))); - } - - private void InspectAssembly (Assembly assembly) - { - foreach (var type in assembly.GetExportedTypes()) - InspectStatic(type); - foreach (var attr in assembly.CustomAttributes) - InspectModules(attr); - } - - private void InspectStatic (Type type) - { - if (type.Namespace?.StartsWith("Bootsharp.Generated") ?? false) return; - foreach (var evt in type.GetEvents(BindingFlags.Public | BindingFlags.Static)) - if (ResolveInterop(evt) is { } ik) - statics.Add(members.Inspect(evt, ik)); - foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Static)) - if (ResolveInterop(prop) is { } ik) - statics.Add(members.Inspect(prop, ik)); - foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) - if (ResolveInterop(method) is { } ik) - statics.Add(members.Inspect(method, ik)); - } - - private void InspectModules (CustomAttributeData attr) - { - if (ResolveInterop(attr) is not { } ik) return; - foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) - if (itd.InspectModule((Type)arg.Value!, ik) is { } md) - modules.Add(md); - } - - private InteropKind? ResolveInterop (MemberInfo info) - { - foreach (var attr in info.CustomAttributes) - if (ResolveInterop(attr) is { } ik) - return ik; - return null; - } - - private InteropKind? ResolveInterop (CustomAttributeData attr) - { - if (attr.AttributeType.FullName == typeof(ExportAttribute).FullName) return InteropKind.Export; - if (attr.AttributeType.FullName == typeof(ImportAttribute).FullName) return InteropKind.Import; - return null; - } -} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs deleted file mode 100644 index c8558210..00000000 --- a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Describes a mutable CLR type whose instances are passed by reference when crossing the interop boundary. -/// -internal record InstancedMeta (Type Clr) : TypeMeta(Clr) -{ - /// - /// Whether the type's instances are exported from C# or imported from JavaScript. - /// - public required InteropKind Interop { get; init; } - /// - /// Namespace of the generated C# bindings wrapper. - /// - public required string Namespace { get; init; } - /// - /// Name of the generated C# bindings wrapper. - /// - public required string Name { get; init; } - /// - /// Full type name of the generated C# bindings wrapper. - /// - public string FullName => $"{Namespace}.{Name}"; - /// - /// Name of the generated JavaScript bindings wrapper. - /// - public required string JSName { get; init; } - /// - /// Members declared on the instance. - /// - public required IReadOnlyCollection Members { get; init; } - /// - /// Name of the specialized C# exporter method or null when is sufficient. - /// - public string? Exporter { get; init; } - /// - /// Name of the specialized JS importer function or null when is sufficient. - /// - public string? Importer { get; init; } -} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs deleted file mode 100644 index 41806759..00000000 --- a/src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Describes a CLR type specified as interop surface under an assembly-level -/// or attribute. -/// -internal record ModuleMeta (Type Clr) : TypeMeta(Clr) -{ - /// - /// Whether the module is exported from C# or imported from JavaScript. - /// - public required InteropKind Interop { get; init; } - /// - /// Namespace of the generated C# bindings wrapper. - /// - public required string Namespace { get; init; } - /// - /// Name of the generated C# bindings wrapper. - /// - public required string Name { get; init; } - /// - /// Full type name of the generated C# bindings wrapper. - /// - public string FullName => $"{Namespace}.{Name}"; - /// - /// Members declared on the module. - /// - public required IReadOnlyCollection Members { get; init; } -} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs deleted file mode 100644 index fb94a3be..00000000 --- a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Describes a CLR type that either crosses the interop boundary directly, or is referenced by such a type. -/// -internal record TypeMeta (Type Clr) -{ - /// - /// The described CLR type. - /// - public Type Clr { get; } = Clr; - /// - /// Fully qualified C# syntax of the type. - /// - public string Syntax { get; } = BuildSyntax(Clr); -} diff --git a/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs b/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs index c4374cd4..27eb6100 100644 --- a/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs +++ b/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs @@ -5,8 +5,12 @@ internal sealed record Preferences { /// public IReadOnlyList Space { get; init; } = []; - /// - public IReadOnlyList Type { get; init; } = []; - /// - public IReadOnlyList Function { get; init; } = []; + /// + public IReadOnlyList Name { get; init; } = []; + /// + public IReadOnlyList Method { get; init; } = []; + /// + public IReadOnlyList Property { get; init; } = []; + /// + public IReadOnlyList Event { get; init; } = []; } diff --git a/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs b/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs index 17084891..82395635 100644 --- a/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs +++ b/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs @@ -2,18 +2,23 @@ namespace Bootsharp.Publish; -internal sealed class PreferencesResolver (string entryAssemblyName) +internal static class PreferencesResolver { - public Preferences Resolve (string outDir) + /// + /// Resolved preferences of the current build task. + /// + internal static AsyncLocal Resolved { get; } = new(); + + public static void Resolve (string entryAssemblyName, string outDir) { using var ctx = CreateLoadContext(outDir); var assemblyPath = Path.Combine(outDir, entryAssemblyName); var assembly = ctx.LoadFromAssemblyPath(assemblyPath); var attribute = FindPreferencesAttribute(assembly); - return CreatePreferences(attribute); + Resolved.Value = CreatePreferences(attribute); } - private CustomAttributeData? FindPreferencesAttribute (Assembly assembly) + private static CustomAttributeData? FindPreferencesAttribute (Assembly assembly) { foreach (var attr in assembly.CustomAttributes) if (attr.AttributeType.FullName == typeof(PreferencesAttribute).FullName) @@ -21,13 +26,15 @@ public Preferences Resolve (string outDir) return null; } - private Preferences CreatePreferences (CustomAttributeData? attr) => new() { + private static Preferences CreatePreferences (CustomAttributeData? attr) => new() { Space = CreatePreferences(nameof(PreferencesAttribute.Space), attr) ?? [], - Type = CreatePreferences(nameof(PreferencesAttribute.Type), attr) ?? [], - Function = CreatePreferences(nameof(PreferencesAttribute.Function), attr) ?? [] + Name = CreatePreferences(nameof(PreferencesAttribute.Name), attr) ?? [], + Method = CreatePreferences(nameof(PreferencesAttribute.Method), attr) ?? [], + Property = CreatePreferences(nameof(PreferencesAttribute.Property), attr) ?? [], + Event = CreatePreferences(nameof(PreferencesAttribute.Event), attr) ?? [] }; - private Preference[]? CreatePreferences (string name, CustomAttributeData? attr) + private static Preference[]? CreatePreferences (string name, CustomAttributeData? attr) { if (attr is null || !attr.NamedArguments.Any(a => a.MemberName == name)) return null; var value = CreateValue(attr.NamedArguments.First(a => a.MemberName == name).TypedValue); @@ -37,7 +44,7 @@ public Preferences Resolve (string outDir) return prefs; } - private string[] CreateValue (CustomAttributeTypedArgument arg) + private static string[] CreateValue (CustomAttributeTypedArgument arg) { var items = ((IEnumerable)arg.Value!).ToArray(); var value = new string[items.Length]; diff --git a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs deleted file mode 100644 index 4aafc5a5..00000000 --- a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs +++ /dev/null @@ -1,121 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Generates binding wrappers for imported instances and instance-specific export handlers. -/// -internal sealed class InstanceGenerator -{ - private InstancedMeta it = null!; - - public string Generate (SolutionInspection spec) => - $$""" - #nullable enable - #pragma warning disable - - using System.Runtime.CompilerServices; - using System.Runtime.InteropServices.JavaScript; - - namespace Bootsharp.Generated - { - public static partial class Instances - { - internal static int Export (T instance, global::System.Func? factory = null) where T : class => global::Bootsharp.Instances.Export(instance, factory); - internal static T Exported (int id) where T : class => global::Bootsharp.Instances.Exported(id); - internal static T Import (int id, global::System.Func factory) where T : class => global::Bootsharp.Instances.Import(id, factory); - - internal static void DisposeImported (int id) - { - NotifyImportedDisposed(id); - global::Bootsharp.Instances.DisposeImported(id); - } - - {{Fmt(spec.Instanced.Where(i => i.Exporter != null).Select(EmitExporter), 2, "\n\n")}} - - [JSExport] private static void DisposeExported (int id) => global::Bootsharp.Instances.DisposeExported(id); - [JSImport("instances.disposeImported", "Bootsharp")] private static partial void NotifyImportedDisposed (int id); - } - } - - {{Fmt(spec.Instanced.Where(i => i.Interop == InteropKind.Import).Select(EmitWrapper), 0, "\n\n")}} - """; - - private static string EmitExporter (InstancedMeta it) - { - var evt = it.Members.OfType().ToArray(); - return - $$""" - internal static int {{it.Exporter}} ({{it.Syntax}} instance) => Export(instance, static (_id, instance) => { - {{Fmt(evt.Select(e => $"instance.{e.Name} += Handle{e.Name};"))}} - return () => { - {{Fmt(evt.Select(e => $"instance.{e.Name} -= Handle{e.Name};"), 2)}} - }; - - {{Fmt(evt.Select(e => { - var args = string.Join(", ", e.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(Export))); - var name = $"{e.JSSpace.Replace('.', '_')}_Broadcast{e.Name}_Serialized"; - return $"void Handle{e.Name} ({args}) => Interop.{name}({invArgs});"; - }))}} - }); - """; - } - - private string EmitWrapper (InstancedMeta it) => - $$""" - namespace {{(this.it = it).Namespace}} - { - public class {{it.Name}} (global::System.Int32 id) : {{it.Syntax}} - { - internal readonly global::System.Int32 _id = id; - - ~{{it.Name}}() => Instances.DisposeImported(_id); - - {{Fmt(it.Members.Select(EmitMemberImport), 2)}} - } - } - """; - - private string EmitMemberImport (MemberMeta member) => member switch { - EventMeta evt => EmitEventImport(evt), - PropertyMeta prop => EmitPropertyImport(prop), - _ => EmitMethodImport((MethodMeta)member), - }; - - private string EmitEventImport (EventMeta evt) - { - var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullity(evt.Info)); - var args = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var callArgs = string.Join(", ", evt.Arguments.Select(a => a.Name)); - return Fmt(0, - $"public event {type} {evt.Name};", - $"internal void Invoke{evt.Name} ({args}) => {evt.Name}?.Invoke({callArgs});" - ); - } - - private string EmitPropertyImport (PropertyMeta prop) - { - var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; - var space = $"global::Bootsharp.Generated.Interop.{it.FullName.Replace('.', '_')}"; - var getArgs = PrependIdArg(""); - var setArgs = PrependIdArg("value"); - return - $$""" - {{type}} {{it.Syntax}}.{{prop.Name}} - { - {{Fmt( - prop.CanGet ? $"get => {space}_GetProperty{prop.Name}({getArgs});" : null, - prop.CanSet ? $"set => {space}_SetProperty{prop.Name}({setArgs});" : null - )}} - } - """; - } - - private string EmitMethodImport (MethodMeta method) - { - var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var callArgs = PrependIdArg(string.Join(", ", method.Arguments.Select(a => a.Name))); - var name = $"{it.FullName.Replace('.', '_')}_{method.Name}"; - return $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name} ({args}) => " + - $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; - } -} diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs deleted file mode 100644 index 4844266c..00000000 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Bootsharp.Publish; - -/// -/// Generates bindings to be picked by .NET's interop source generator. -/// -internal sealed class InteropGenerator -{ - [MemberNotNullWhen(true, nameof(it))] - private bool isIt => it != null; - [MemberNotNullWhen(true, nameof(md))] - private bool isMd => md != null; - - private string id = null!, space = null!; - private InstancedMeta? it; - private ModuleMeta? md; - - public string Generate (SolutionInspection spec) => - $$""" - #nullable enable - #pragma warning disable - - using System.Runtime.CompilerServices; - using System.Runtime.InteropServices.JavaScript; - - namespace Bootsharp.Generated; - - public static partial class Interop - { - [ModuleInitializer] - internal static unsafe void Initialize () - { - {{Fmt([ - ..spec.Static.OfType() - .Where(e => e.Interop == InteropKind.Export) - .Select(e => EmitStaticEventSubscription(e, e.Space)), - ..spec.Modules.SelectMany(md => md.Members.OfType() - .Where(e => e.Interop == InteropKind.Export) - .Select(e => EmitStaticEventSubscription(e, md.FullName))), - ..spec.Static.OfType() - .Where(m => m.Interop == InteropKind.Import) - .Select(EmitStaticMethodAssignment), - ..spec.Static.OfType() - .Where(p => p.Interop == InteropKind.Import) - .SelectMany(EmitStaticPropertyAssignment) - ], 2)}} - } - - {{Fmt(spec.Static.SelectMany(m => EmitMember(m, null, null)))}} - {{Fmt(spec.Modules.SelectMany(md => md.Members.SelectMany(m => EmitMember(m, null, md))))}} - {{Fmt(spec.Instanced.SelectMany(it => it.Members.SelectMany(m => EmitMember(m, it, null))))}} - } - """; - - private static string EmitStaticEventSubscription (EventMeta evt, string space) - { - var handler = $"Handle_{space.Replace('.', '_')}_{evt.Name}"; - return $"global::{space}.{evt.Name} += {handler};"; - } - - private static string EmitStaticMethodAssignment (MethodMeta method) - { - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; - } - - private static IEnumerable EmitStaticPropertyAssignment (PropertyMeta p) - { - var id = p.Space.Replace('.', '_'); - if (p.CanGet) yield return $"global::{p.Space}.Bootsharp_GetProperty{p.Name} = &{id}_GetProperty{p.Name};"; - if (p.CanSet) yield return $"global::{p.Space}.Bootsharp_SetProperty{p.Name} = &{id}_SetProperty{p.Name};"; - } - - private IEnumerable EmitMember (MemberMeta member, InstancedMeta? it, ModuleMeta? md) - { - this.it = it; - this.md = md; - space = it?.FullName ?? md?.FullName ?? member.Space; - id = space.Replace('.', '_'); - return member switch { - EventMeta { Interop: InteropKind.Export } e => EmitEventExport(e), - EventMeta { Interop: InteropKind.Import } e => EmitEventImport(e), - PropertyMeta { Interop: InteropKind.Export } p => EmitPropertyExport(p), - PropertyMeta { Interop: InteropKind.Import } p => EmitPropertyImport(p), - MethodMeta { Interop: InteropKind.Export } m => EmitMethodExport(m), - _ => EmitMethodImport((MethodMeta)member) - }; - } - - private IEnumerable EmitEventExport (EventMeta evt) - { - var attr = $"""[JSImport("{evt.JSSpace}.broadcast{evt.Name}Serialized", "Bootsharp")] """; - var name = $"{evt.JSSpace.Replace('.', '_')}_Broadcast{evt.Name}_Serialized"; - var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - yield return $"{attr}internal static partial void {name} ({args});"; - - if (isIt) yield break; // instanced export event handlers are emitted by InstanceGenerator - var handler = $"Handle_{id}_{evt.Name}"; - var sigArgs = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var invArgs = string.Join(", ", evt.Arguments.Select(Export)); - yield return $"private static void {handler} ({sigArgs}) => {name}({invArgs});"; - } - - private IEnumerable EmitEventImport (EventMeta evt) - { - var name = $"{id}_Invoke{evt.Name}"; - var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var invName = isIt ? $"Instances.Import(_id, static id => new global::{it.FullName}(id)).Invoke{evt.Name}" - : isMd ? $"((global::{md.FullName})Modules.Imports[typeof({md.Syntax})].Instance).Invoke{evt.Name}" - : $"global::{evt.Info.DeclaringType!.FullName!.Replace('+', '.')}.Bootsharp_Invoke_{evt.Name}"; - var invArgs = string.Join(", ", evt.Arguments.Select(Import)); - yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; - } - - private IEnumerable EmitPropertyExport (PropertyMeta prop) - { - if (prop.CanGet) - { - var attr = $"[JSExport] {MarshalAmbiguous(prop.GetValue, true)}"; - var name = $"{id}_GetProperty{prop.Name}"; - var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; - var body = Export(prop.GetValue, isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name}" - : isMd ? $"global::{space}.GetProperty{prop.Name}()" - : $"global::{space}.{prop.Name}"); - yield return $"{attr}internal static {BuildValueSyntax(prop.GetValue)} {name} ({args}) => {body};"; - } - if (prop.CanSet) - { - var name = $"{id}_SetProperty{prop.Name}"; - var args = BuildParameter(prop.SetValue, "value"); - if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var value = Import(prop.SetValue, "value"); - var body = isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name} = {value}" - : isMd ? $"global::{space}.SetProperty{prop.Name}({value})" - : $"global::{space}.{prop.Name} = {value}"; - yield return $"[JSExport] internal static void {name} ({args}) => {body};"; - } - } - - private IEnumerable EmitPropertyImport (PropertyMeta prop) - { - if (prop.CanGet) - { - var endpoint = $"""("{prop.JSSpace}.getProperty{prop.Name}Serialized", "Bootsharp")"""; - var attr = $"[JSImport{endpoint}] {MarshalAmbiguous(prop.GetValue, true)}"; - var serdeName = $"{prop.JSSpace.Replace('.', '_')}_GetProperty{prop.Name}_Serialized"; - var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; - yield return $"{attr}internal static partial {BuildValueSyntax(prop.GetValue)} {serdeName} ({args});"; - - var name = $"{id}_GetProperty{prop.Name}"; - var body = Import(prop.GetValue, isIt ? $"{serdeName}(_id)" : $"{serdeName}()"); - yield return $"public static {prop.GetValue.TypeSyntax} {name}({args}) => {body};"; - } - if (prop.CanSet) - { - var attr = $"""[JSImport("{prop.JSSpace}.setProperty{prop.Name}Serialized", "Bootsharp")] """; - var serdeName = $"{prop.JSSpace.Replace('.', '_')}_SetProperty{prop.Name}_Serialized"; - var serdeArgs = BuildParameter(prop.SetValue, "value"); - if (isIt) serdeArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(serdeArgs)}"; - yield return $"{attr}internal static partial void {serdeName} ({serdeArgs});"; - - var name = $"{id}_SetProperty{prop.Name}"; - var args = $"{prop.SetValue.TypeSyntax} value"; - if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var value = Export(prop.SetValue, "value"); - var body = isIt ? $"{serdeName}(_id, {value})" : $"{serdeName}({value})"; - yield return $"public static void {name}({args}) => {body};"; - } - } - - private IEnumerable EmitMethodExport (MethodMeta method) - { - var wait = ShouldWait(method); - var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; - var name = $"{id}_{method.Name}"; - var @return = BuildValueSyntax(method.Return); - if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; - var sigArgs = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; - var invArgs = string.Join(", ", method.Arguments.Select(Import)); - var invName = isIt - ? $"Instances.Exported<{it.Syntax}>(_id).{method.Name}" - : $"global::{space}.{method.Name}"; - var body = Export(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); - yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; - } - - private IEnumerable EmitMethodImport (MethodMeta method) - { - var marshalAs = MarshalAmbiguous(method.Return, true); - var attr = $"""[JSImport("{method.JSSpace}.{method.JSName}Serialized", "Bootsharp")] {marshalAs}"""; - var name = $"{id}_{method.Name}"; - var @return = BuildValueSyntax(method.Return); - if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; - var args = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - yield return $"{attr}internal static partial {@return} {name}_Serialized ({args});"; - - var wait = ShouldWait(method); - @return = $"{(wait ? "async " : "")}{method.Return.TypeSyntax}"; - var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; - var invArgs = string.Join(", ", method.Arguments.Select(Export)); - if (isIt) invArgs = PrependIdArg(invArgs); - var body = Import(method.Return, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); - yield return $"public static {@return} {name} ({sigArgs}) => {body};"; - } - - private string BuildParameter (ValueMeta value, string name) - { - var type = BuildValueSyntax(value); - return $"{MarshalAmbiguous(value, false)}{type} {name}"; - } - - private string BuildValueSyntax (ValueMeta value) - { - var nil = value.Nullable && !value.IsSerialized ? "?" : ""; - if (value.IsInstanced) return $"global::System.Int32{nil}"; - if (value.IsSerialized) return $"global::System.Int64{nil}"; - return value.TypeSyntax; - } - - private static string MarshalAmbiguous (ValueMeta value, bool @return) - { - var stx = value.TypeSyntax; - var promise = stx.StartsWith("global::System.Threading.Tasks.Task<"); - if (promise) stx = stx[36..]; - - var result = ""; - if (value.IsSerialized || stx.StartsWith("global::System.Int64")) result = "JSType.BigInt"; - else if (stx.StartsWith("global::System.DateTime")) result = "JSType.Date"; - if (result == "") return ""; - - if (promise) result = $"JSType.Promise<{result}>"; - if (@return) return $"[return: JSMarshalAs<{result}>] "; - return $"[JSMarshalAs<{result}>] "; - } - - private bool ShouldWait (MethodMeta method) - { - if (!method.Async) return false; - return method.Return.IsSerialized || method.Return.IsInstanced; - } -} diff --git a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs b/src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs similarity index 65% rename from src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs rename to src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs index 6ecfaf5d..59acb866 100644 --- a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/GenerateCS.cs @@ -3,7 +3,7 @@ namespace Bootsharp.Publish; /// /// First pass: emits C# sources to be picked by .NET's source generators. /// -public sealed class BootsharpEmit : Microsoft.Build.Utilities.Task +public sealed class GenerateCS : Microsoft.Build.Utilities.Task { public required string InspectedDirectory { get; set; } public required string EntryAssemblyName { get; set; } @@ -14,24 +14,18 @@ public sealed class BootsharpEmit : Microsoft.Build.Utilities.Task public override bool Execute () { - var prefs = ResolvePreferences(); - using var inspection = InspectSolution(prefs); - GenerateSerializer(inspection); - GenerateInstances(inspection); - GenerateModules(inspection); - GenerateInterop(inspection); + PreferencesResolver.Resolve(EntryAssemblyName, InspectedDirectory); + using var spec = InspectSolution(); + GenerateSerializer(spec); + GenerateInstances(spec); + GenerateModules(spec); + GenerateInterop(spec); return true; } - private Preferences ResolvePreferences () + private SolutionInspection InspectSolution () { - var resolver = new PreferencesResolver(EntryAssemblyName); - return resolver.Resolve(InspectedDirectory); - } - - private SolutionInspection InspectSolution (Preferences prefs) - { - var inspector = new SolutionInspector(prefs); + var inspector = new SolutionInspector(); var inspected = Directory.GetFiles(InspectedDirectory, "*.dll").Order(); var inspection = inspector.Inspect(InspectedDirectory, inspected); new InspectionReporter(Log).Report(inspection); @@ -41,28 +35,32 @@ private SolutionInspection InspectSolution (Preferences prefs) private void GenerateSerializer (SolutionInspection spec) { var generator = new SerializerGenerator(); - var content = generator.Generate(spec); + var serialized = spec.Types.OfType().ToArray(); + var content = generator.Generate(serialized); WriteGenerated(SerializerFilePath, content); } private void GenerateInstances (SolutionInspection spec) { var generator = new InstanceGenerator(); - var content = generator.Generate(spec); + var instanced = spec.Types.OfType().ToArray(); + var content = generator.Generate(instanced); WriteGenerated(InstancesFilePath, content); } private void GenerateModules (SolutionInspection spec) { var generator = new ModuleGenerator(); - var content = generator.Generate(spec); + var mds = spec.Types.OfType().ToArray(); + var content = generator.Generate(mds); WriteGenerated(ModulesFilePath, content); } private void GenerateInterop (SolutionInspection spec) { var generator = new InteropGenerator(); - var content = generator.Generate(spec); + var surfaces = spec.Types.OfType().ToArray(); + var content = generator.Generate(surfaces); WriteGenerated(InteropFilePath, content); } diff --git a/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs new file mode 100644 index 00000000..8fddbeb9 --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateCS/InstanceGenerator.cs @@ -0,0 +1,129 @@ +namespace Bootsharp.Publish; + +/// +/// Generates binding proxies for imported instances and instance-specific export handlers. +/// +internal sealed class InstanceGenerator +{ + private InstanceMeta it = null!; + + public string Generate (IReadOnlyCollection its) => + $$""" + #nullable enable + #pragma warning disable + + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices.JavaScript; + + namespace Bootsharp.Generated + { + public static partial class Instances + { + internal static int Export (T it, Bootsharp.Instances.ExportCallback? cb = null) where T : class => Bootsharp.Instances.Export(it, cb); + internal static 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(EmitProxy), 0, "\n\n")}} + """; + + private static string EmitImporter (InstanceMeta it) + { + var proxy = $"static id => new {it.Proxy.Syntax}(id)"; + return $"Bootsharp.Instances.RegisterImport(typeof({it.Syntax}), {proxy});"; + } + + private static string EmitExporter (InstanceMeta it) + { + var evt = it.Members.OfType().ToArray(); + return + $$""" + internal static int {{it.Exporter}} ({{it.Syntax}} it) => Export(it, static (_id, it) => { + {{Fmt(evt.Select(e => $"it.{e.Name} += Handle{e.Name};"))}} + return () => { + {{Fmt(evt.Select(e => $"it.{e.Name} -= Handle{e.Name};"), 2)}} + }; + + {{Fmt(evt.Select(e => { + var args = string.Join(", ", e.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var invArgs = PrependIdArg(string.Join(", ", e.Args.Select(Export))); + var name = $"{it.Id}_Broadcast{e.Name}_Serialized"; + return $"void Handle{e.Name} ({args}) => Interop.{name}({invArgs});"; + }))}} + }); + """; + } + + private string EmitProxy (InstanceMeta it) => + $$""" + namespace {{(this.it = it).Proxy.Space}} + { + public class {{it.Proxy.Name}} (int id) : Bootsharp.JSProxy(id), {{it.Syntax}} + { + ~{{it.Proxy.Name}}() => Instances.DisposeImported(_id); + + {{Fmt(it.Members.Select(EmitMemberImport), 2)}} + } + } + """; + + private string EmitMemberImport (MemberMeta member) => member switch { + EventMeta evt => EmitEventImport(evt), + PropertyMeta prop => EmitPropertyImport(prop), + _ => EmitMethodImport((MethodMeta)member), + }; + + private string EmitEventImport (EventMeta evt) + { + var args = string.Join(", ", evt.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = string.Join(", ", evt.Args.Select(a => a.Name)); + return Fmt(0, + $"public event {evt.TypeSyntax} {evt.Name};", + $"internal void Invoke{evt.Name} ({args}) => {evt.Name}?.Invoke({callArgs});" + ); + } + + private string EmitPropertyImport (PropertyMeta prop) + { + var space = $"global::Bootsharp.Generated.Interop.{it.Proxy.Id}"; + var getArgs = PrependIdArg(""); + var setArgs = PrependIdArg("value"); + return + $$""" + {{prop.TypeSyntax}} {{it.Syntax}}.{{prop.Name}} + { + {{Fmt( + prop.CanGet ? $"get => {space}_Get{prop.Name}({getArgs});" : null, + prop.CanSet ? $"set => {space}_Set{prop.Name}({setArgs});" : null + )}} + } + """; + } + + private string EmitMethodImport (MethodMeta method) + { + var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = PrependIdArg(string.Join(", ", method.Args.Select(a => a.Name))); + var name = $"{it.Proxy.Id}_{method.Name}"; + return $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name} ({args}) => " + + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; + } +} diff --git a/src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs new file mode 100644 index 00000000..34a62ccc --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateCS/InteropGenerator.cs @@ -0,0 +1,230 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Bootsharp.Publish; + +/// +/// Generates bindings to be picked by .NET's interop source generator. +/// +internal sealed class InteropGenerator +{ + [MemberNotNullWhen(true, nameof(it))] + private bool isIt => srf is InstanceMeta; + [MemberNotNullWhen(true, nameof(md))] + private bool isMd => srf is ModuleMeta; + + private string id = null!, stx = null!; + private SurfaceMeta srf = null!; + private InstanceMeta? it => srf as InstanceMeta; + private ModuleMeta? md => srf as ModuleMeta; + + public string Generate (IReadOnlyCollection srf) => + $$""" + #nullable enable + #pragma warning disable + + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices.JavaScript; + + namespace Bootsharp.Generated; + + public static partial class Interop + { + [ModuleInitializer] + internal static unsafe void Initialize () + { + {{Fmt(srf.SelectMany(EmitInitializers), 2)}} + } + + {{Fmt(srf.SelectMany(s => s.Members.SelectMany(m => EmitMember(m, s))))}} + } + """; + + private static IEnumerable EmitInitializers (SurfaceMeta srf) + { + if (srf is InstanceMeta) yield break; + var id = (srf as ProxyMeta)?.Proxy.Id ?? srf.Id; + var stx = (srf as ProxyMeta)?.Proxy.Syntax ?? srf.Syntax; + foreach (var evt in srf.Members.OfType().Where(e => e.IK == InteropKind.Export)) + yield return $"{stx}.{evt.Name} += Handle_{id}_{evt.Name};"; + if (srf is not StaticMeta) yield break; + foreach (var mem in srf.Members.OfType().Where(m => m.IK == InteropKind.Import)) + yield return $"{srf.Syntax}.Bootsharp_{mem.Name} = &{srf.Id}_{mem.Name};"; + foreach (var p in srf.Members.OfType().Where(p => p.IK == InteropKind.Import)) + { + if (p.CanGet) yield return $"{srf.Syntax}.Bootsharp_Get{p.Name} = &{srf.Id}_Get{p.Name};"; + if (p.CanSet) yield return $"{srf.Syntax}.Bootsharp_Set{p.Name} = &{srf.Id}_Set{p.Name};"; + } + } + + private IEnumerable EmitMember (MemberMeta member, SurfaceMeta srf) + { + this.srf = srf; + id = (srf as ProxyMeta)?.Proxy.Id ?? srf.Id; + stx = (srf as ProxyMeta)?.Proxy.Syntax ?? srf.Syntax; + return member switch { + EventMeta { IK: InteropKind.Export } e => EmitEventExport(e), + EventMeta { IK: InteropKind.Import } e => EmitEventImport(e), + PropertyMeta { IK: InteropKind.Export } p => EmitPropertyExport(p), + PropertyMeta { IK: InteropKind.Import } p => EmitPropertyImport(p), + MethodMeta { IK: InteropKind.Export } m => EmitMethodExport(m), + _ => EmitMethodImport((MethodMeta)member) + }; + } + + private IEnumerable EmitEventExport (EventMeta evt) + { + var attr = $"""[JSImport("{srf.JSNode}.broadcast{evt.Name}Serialized", "{srf.JSModule}")] """; + var name = $"{srf.Id}_Broadcast{evt.Name}_Serialized"; + var args = string.Join(", ", evt.Args.Select(a => BuildParameter(a.Value, a.Name))); + if (isIt) args = $"int {PrependIdArg(args)}"; + yield return $"{attr}internal static partial void {name} ({args});"; + + if (isIt) yield break; // instance event handlers are emitted by InstanceGenerator + var handler = $"Handle_{id}_{evt.Name}"; + var sigArgs = string.Join(", ", evt.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var invArgs = string.Join(", ", evt.Args.Select(Export)); + yield return $"private static void {handler} ({sigArgs}) => {name}({invArgs});"; + } + + private IEnumerable EmitEventImport (EventMeta evt) + { + var name = $"{id}_Invoke{evt.Name}"; + var args = string.Join(", ", evt.Args.Select(a => BuildParameter(a.Value, a.Name))); + if (isIt) args = $"int {PrependIdArg(args)}"; + var invName = isIt ? $"(({it.Proxy.Syntax})Instances.Resolve<{it.Syntax}>(_id)).Invoke{evt.Name}" + : isMd ? $"(({md.Proxy.Syntax})Modules.Imports[typeof({md.Syntax})].Instance).Invoke{evt.Name}" + : $"{srf.Syntax}.Bootsharp_Invoke_{evt.Name}"; + var invArgs = string.Join(", ", evt.Args.Select(Import)); + yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; + } + + private IEnumerable EmitPropertyExport (PropertyMeta prop) + { + if (prop.CanGet) + { + var attr = $"[JSExport] {MarshalAmbiguous(prop.Get, true)}"; + var name = $"{id}_Get{prop.Name}"; + var args = isIt ? "int _id" : ""; + var body = Export(prop.Get, isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name}" + : isMd ? $"{stx}.Get{prop.Name}()" + : $"{stx}.{prop.Name}"); + yield return $"{attr}internal static {BuildValueSyntax(prop.Get)} {name} ({args}) => {body};"; + } + if (prop.CanSet) + { + var name = $"{id}_Set{prop.Name}"; + var args = BuildParameter(prop.Set, "value"); + if (isIt) args = $"int {PrependIdArg(args)}"; + var value = Import(prop.Set, "value"); + var body = isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name} = {value}" + : isMd ? $"{stx}.Set{prop.Name}({value})" + : $"{stx}.{prop.Name} = {value}"; + yield return $"[JSExport] internal static void {name} ({args}) => {body};"; + } + } + + private IEnumerable EmitPropertyImport (PropertyMeta prop) + { + if (prop.CanGet) + { + var endpoint = $"""("{srf.JSNode}.get{prop.Name}Serialized", "{srf.JSModule}")"""; + var attr = $"[JSImport{endpoint}] {MarshalAmbiguous(prop.Get, true)}"; + var srdName = $"{srf.Id}_Get{prop.Name}_Serialized"; + var args = isIt ? "int _id" : ""; + yield return $"{attr}internal static partial {BuildValueSyntax(prop.Get)} {srdName} ({args});"; + + var name = $"{id}_Get{prop.Name}"; + var body = Import(prop.Get, isIt ? $"{srdName}(_id)" : $"{srdName}()"); + yield return $"public static {prop.Get.TypeSyntax} {name}({args}) => {body};"; + } + if (prop.CanSet) + { + var attr = $"""[JSImport("{srf.JSNode}.set{prop.Name}Serialized", "{srf.JSModule}")]"""; + var srdName = $"{srf.Id}_Set{prop.Name}_Serialized"; + var srdArgs = BuildParameter(prop.Set, "value"); + if (isIt) srdArgs = $"int {PrependIdArg(srdArgs)}"; + yield return $"{attr} internal static partial void {srdName} ({srdArgs});"; + + var name = $"{id}_Set{prop.Name}"; + var args = $"{prop.Set.TypeSyntax} value"; + if (isIt) args = $"int {PrependIdArg(args)}"; + var value = Export(prop.Set, "value"); + var body = isIt ? $"{srdName}(_id, {value})" : $"{srdName}({value})"; + yield return $"public static void {name}({args}) => {body};"; + } + } + + private IEnumerable EmitMethodExport (MethodMeta method) + { + var wait = ShouldWait(method); + var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; + var name = $"{id}_{method.Name}"; + var @return = BuildValueSyntax(method.Return); + if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; + var sigArgs = string.Join(", ", method.Args.Select(a => BuildParameter(a.Value, a.Name))); + if (isIt) sigArgs = $"int {PrependIdArg(sigArgs)}"; + var invArgs = string.Join(", ", method.Args.Select(Import)); + var invName = isIt + ? $"Instances.Exported<{it.Syntax}>(_id).{method.Name}" + : $"{stx}.{method.Name}"; + var body = Export(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; + } + + private IEnumerable EmitMethodImport (MethodMeta method) + { + var attr = $"""[JSImport("{srf.JSNode}.{method.JSName}Serialized", "{srf.JSModule}")]"""; + var marshalAs = MarshalAmbiguous(method.Return, true); + var name = $"{id}_{method.Name}"; + var @return = BuildValueSyntax(method.Return); + if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; + var args = string.Join(", ", method.Args.Select(a => BuildParameter(a.Value, a.Name))); + if (isIt) args = $"int {PrependIdArg(args)}"; + yield return $"{attr} {marshalAs}internal static partial {@return} {name}_Serialized ({args});"; + + var wait = ShouldWait(method); + @return = $"{(wait ? "async " : "")}{method.Return.TypeSyntax}"; + var sigArgs = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + if (isIt) sigArgs = $"int {PrependIdArg(sigArgs)}"; + var invArgs = string.Join(", ", method.Args.Select(Export)); + if (isIt) invArgs = PrependIdArg(invArgs); + var body = Import(method.Return, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); + yield return $"public static {@return} {name} ({sigArgs}) => {body};"; + } + + private string BuildParameter (ValueMeta value, string name) + { + var type = BuildValueSyntax(value); + return $"{MarshalAmbiguous(value, false)}{type} {name}"; + } + + private string BuildValueSyntax (ValueMeta value) + { + var nil = value.Nullable && !value.IsSerialized ? "?" : ""; + if (value.IsInstanced) return $"int{nil}"; + if (value.IsSerialized) return $"long{nil}"; + return value.TypeSyntax; + } + + private static string MarshalAmbiguous (ValueMeta value, bool @return) + { + var stx = value.TypeSyntax; + var promise = stx.StartsWith("global::System.Threading.Tasks.Task<"); + if (promise) stx = stx[36..]; + + var result = ""; + if (value.IsSerialized || stx.StartsWith("global::System.Int64")) result = "JSType.BigInt"; + else if (stx.StartsWith("global::System.DateTime")) result = "JSType.Date"; + if (result == "") return ""; + + if (promise) result = $"JSType.Promise<{result}>"; + if (@return) return $"[return: JSMarshalAs<{result}>] "; + return $"[JSMarshalAs<{result}>] "; + } + + private bool ShouldWait (MethodMeta method) + { + if (!method.Async) return false; + return method.Return.IsSerialized || method.Return.IsInstanced; + } +} diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs similarity index 56% rename from src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs index a57618d8..3f3836e2 100644 --- a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/ModuleGenerator.cs @@ -7,7 +7,7 @@ internal sealed class ModuleGenerator { private ModuleMeta md = null!; - public string Generate (SolutionInspection spec) => + public string Generate (IReadOnlyCollection mds) => $$""" #nullable enable #pragma warning disable @@ -19,44 +19,44 @@ internal static class ModuleRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterModules () { - {{Fmt(spec.Modules.Select(EmitRegistration), 3)}} + {{Fmt(mds.Select(EmitRegistration), 3)}} } } } - {{Fmt(spec.Modules.Select(EmitModule), 0, "\n\n")}} + {{Fmt(mds.Select(EmitModule), 0, "\n\n")}} """; private string EmitRegistration (ModuleMeta md) { - var type = md.Interop == InteropKind.Import + var type = md.IK == InteropKind.Import ? $"typeof({md.Syntax})" - : $"typeof({md.FullName})"; - var factory = md.Interop == InteropKind.Import - ? $"new ImportModule(new {md.FullName}())" - : $"new ExportModule(typeof({md.Syntax}), handler => new {md.FullName}(({md.Syntax})handler))"; + : $"typeof({md.Proxy.Syntax})"; + var factory = md.IK == InteropKind.Import + ? $"new ImportModule(new {md.Proxy.Syntax}())" + : $"new ExportModule(typeof({md.Syntax}), handler => new {md.Proxy.Syntax}(({md.Syntax})handler))"; return $"Modules.Register({type}, {factory});"; } private string EmitModule (ModuleMeta md) { this.md = md; - if (md.Interop == InteropKind.Export) return EmitModuleExport(); + if (md.IK == InteropKind.Export) return EmitModuleExport(); return EmitModuleImport(); } private string EmitModuleExport () => $$""" - namespace {{md.Namespace}} + namespace {{md.Proxy.Space}} { - public class {{md.Name}} + public class {{md.Proxy.Name}} { private static {{md.Syntax}} handler = null!; - public {{md.Name}} ({{md.Syntax}} handler) + public {{md.Proxy.Name}} ({{md.Syntax}} handler) { {{Fmt([ - $"{md.Name}.handler = handler;", + $"{md.Proxy.Name}.handler = handler;", ..md.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") ], 3)}} } @@ -68,9 +68,9 @@ public class {{md.Name}} private string EmitModuleImport () => $$""" - namespace {{md.Namespace}} + namespace {{md.Proxy.Space}} { - public class {{md.Name}} : {{md.Syntax}} + public class {{md.Proxy.Name}} : {{md.Syntax}} { {{Fmt(md.Members.Select(EmitMemberImport), 2)}} } @@ -86,22 +86,20 @@ public class {{md.Name}} : {{md.Syntax}} private string EmitMemberImport (MemberMeta member) => member switch { EventMeta evt => EmitEventImport(evt), PropertyMeta prop => EmitPropertyImport(prop), - _ => EmitMethodImport((MethodMeta)member), + _ => EmitMethodImport((MethodMeta)member) }; private string EmitEventExport (EventMeta evt) { - var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullity(evt.Info)); - return $"[Export] public static event {type} {evt.Name};"; + return $"[Export] public static event {evt.TypeSyntax} {evt.Name};"; } private string EmitEventImport (EventMeta evt) { - var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullity(evt.Info)); - var args = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var callArgs = string.Join(", ", evt.Arguments.Select(a => a.Name)); + var args = string.Join(", ", evt.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = string.Join(", ", evt.Args.Select(a => a.Name)); return Fmt(0, - $"public event {type} {evt.Name};", + $"public event {evt.TypeSyntax} {evt.Name};", $"internal void Invoke{evt.Name} ({args}) => {evt.Name}?.Invoke({callArgs});" ); } @@ -109,23 +107,22 @@ private string EmitEventImport (EventMeta evt) private string EmitPropertyExport (PropertyMeta prop) { var name = prop.Name; - var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; - var get = $"[Export] public static {type} GetProperty{name} () => handler.{name};"; - var set = $"[Export] public static void SetProperty{name} ({type} value) => handler.{name} = value;"; + var type = prop.TypeSyntax; + var get = $"[Export] public static {type} Get{name} () => handler.{name};"; + var set = $"[Export] public static void Set{name} ({type} value) => handler.{name} = value;"; return Fmt(0, prop.CanGet ? get : null, prop.CanSet ? set : null); } private string EmitPropertyImport (PropertyMeta prop) { - var space = $"global::Bootsharp.Generated.Interop.{md.FullName.Replace('.', '_')}"; - var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; + var space = $"global::Bootsharp.Generated.Interop.{md.Proxy.Id}"; return $$""" - {{type}} {{md.Syntax}}.{{prop.Name}} + {{prop.TypeSyntax}} {{md.Syntax}}.{{prop.Name}} { {{Fmt( - prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, - prop.CanSet ? $"set => {space}_SetProperty{prop.Name}(value);" : null + prop.CanGet ? $"get => {space}_Get{prop.Name}();" : null, + prop.CanSet ? $"set => {space}_Set{prop.Name}(value);" : null )}} } """; @@ -133,17 +130,17 @@ private string EmitPropertyImport (PropertyMeta prop) private string EmitMethodExport (MethodMeta method) { - var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var sig = $"public static {method.Return.TypeSyntax} {method.Name} ({args})"; - var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); + var callArgs = string.Join(", ", method.Args.Select(a => a.Name)); return $"[Export] {sig} => handler.{method.Name}({callArgs});"; } private string EmitMethodImport (MethodMeta method) { - var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); - var name = $"{md.FullName.Replace('.', '_')}_{method.Name}"; + var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = string.Join(", ", method.Args.Select(a => a.Name)); + var name = $"{md.Proxy.Id}_{method.Name}"; return $"{method.Return.TypeSyntax} {md.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/SerializerGenerator.cs similarity index 51% rename from src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateCS/SerializerGenerator.cs index 31b6e4a1..f5401fac 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/SerializerGenerator.cs @@ -2,23 +2,21 @@ namespace Bootsharp.Publish; internal sealed class SerializerGenerator { - public string Generate (SolutionInspection spec) - { - var serialized = spec.Serialized; - if (serialized.Count == 0) return ""; - return $$""" - using System.Runtime.CompilerServices; + public string Generate (IReadOnlyCollection srd) => srd.Count == 0 ? "" : + $$""" + using System.Runtime.CompilerServices; - namespace Bootsharp.Generated; + namespace Bootsharp.Generated; - internal static class SerializerContext - { - {{Fmt(serialized.Select(EmitFactory))}} + internal static class SerializerContext + { + {{Fmt(srd.Select(EmitFactory))}} - {{Fmt(serialized.SelectMany(EmitHelpers), separator: "\n\n")}} - } - """; - } + {{Fmt(srd.OfType().Select(EmitInstanced), separator: "\n\n")}} + + {{Fmt(srd.OfType().SelectMany(EmitObject), separator: "\n\n")}} + } + """; private string EmitFactory (SerializedMeta meta) { @@ -40,40 +38,35 @@ static string ResolvePrimitive (Type type) } } - private IEnumerable EmitHelpers (SerializedMeta meta) + private string EmitInstanced (SerializedInstanceMeta it) => + $$""" + private static void Write_{{it.Id}} (ref Writer writer, {{it.Syntax}} value) + { + writer.WriteInt32({{Export(it.Instance, "value")}}); + } + + private static {{it.Syntax}} Read_{{it.Id}} (ref Reader reader) + { + return {{Import(it.Instance, "reader.ReadInt32()")}}; + } + """; + + private IEnumerable EmitObject (SerializedObjectMeta obj) { - if (meta is SerializedInstanceMeta it) - { - yield return - $$""" - private static void Write_{{it.Id}} (ref Writer writer, {{it.Syntax}} value) - { - writer.WriteInt32({{Export(it.Instance, "value")}}); - } - - private static {{it.Syntax}} Read_{{it.Id}} (ref Reader reader) - { - return {{Import(it.Instance, "reader.ReadInt32()")}}; - } - """; - } - if (meta is SerializedObjectMeta obj) - { - yield return - $$""" - private static void Write_{{obj.Id}} (ref Writer writer, {{obj.Syntax}} value) - { - {{Fmt(EmitObjectWrite(obj))}} - } - - private static {{obj.Syntax}} Read_{{obj.Id}} (ref Reader reader) - { - {{Fmt(EmitObjectRead(obj))}} - } - """; - foreach (var prop in obj.Properties.Where(p => p.Kind == SerializedPropertyKind.Field)) - yield return EmitFieldAccessor(obj, prop); - } + yield return + $$""" + private static void Write_{{obj.Id}} (ref Writer writer, {{obj.Syntax}} value) + { + {{Fmt(EmitObjectWrite(obj))}} + } + + private static {{obj.Syntax}} Read_{{obj.Id}} (ref Reader reader) + { + {{Fmt(EmitObjectRead(obj))}} + } + """; + foreach (var p in obj.Properties.Where(p => p.Kind == SerializedPropertyKind.Field)) + yield return EmitFieldAccessor(obj, p); } private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) @@ -84,39 +77,33 @@ private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) yield return "if (value is null) return;"; } foreach (var p in obj.Properties) - if (p.OmitWhenNull) + if (p.Nullable) { yield return $"writer.WriteBool(value.{p.Name} is not null);"; - yield return $"if (value.{p.Name} is not null) {p.Id}.Write(ref writer, value.{p.Name});"; + yield return $"if (value.{p.Name} is not null) {p.Type.Id}.Write(ref writer, value.{p.Name});"; } - else yield return $"{p.Id}.Write(ref writer, value.{p.Name});"; + else yield return $"{p.Type.Id}.Write(ref writer, value.{p.Name});"; } private IEnumerable EmitObjectRead (SerializedObjectMeta obj) { if (!obj.Clr.IsValueType) yield return "if (!reader.ReadBool()) return null!;"; foreach (var p in obj.Properties) - { - var var = MangleLocal(p.Name); - if (p.OmitWhenNull) yield return $"var {var} = reader.ReadBool() ? {p.Id}.Read(ref reader) : default;"; - else yield return $"var {var} = {p.Id}.Read(ref reader);"; - } + if (!p.Nullable) yield return $"var {Var(p)} = {p.Type.Id}.Read(ref reader);"; + else yield return $"var {Var(p)} = reader.ReadBool() ? {p.Type.Id}.Read(ref reader) : default;"; yield return $"var _value_ = {EmitObjectConstruction(obj)};"; - foreach (var p in obj.Properties.Where(p => !p.ConstructorParameter && !ShouldInitializeInConstruction(p))) - { - var var = MangleLocal(p.Name); - if (p.Kind == SerializedPropertyKind.Set) yield return $"_value_.{p.Name} = {var};"; + foreach (var p in obj.Properties.Where(p => !p.Ctor && !ShouldInitializeInConstruction(p))) + if (p.Kind == SerializedPropertyKind.Set) yield return $"_value_.{p.Name} = {Var(p)};"; else if (p.Kind == SerializedPropertyKind.Field) yield return EmitFieldAssign(obj, p); - } yield return "return _value_;"; } private string EmitObjectConstruction (SerializedObjectMeta obj) { - var ctorArgs = obj.Properties.Where(p => p.ConstructorParameter); - var ctor = $"new {obj.Syntax}({string.Join(", ", ctorArgs.Select(p => MangleLocal(p.Name)))})"; - var props = obj.Properties.Where(p => !p.ConstructorParameter && ShouldInitializeInConstruction(p)) - .Select(p => $"{p.Name} = {MangleLocal(p.Name)}").ToArray(); + var ctorArgs = obj.Properties.Where(p => p.Ctor); + var ctor = $"new {obj.Syntax}({string.Join(", ", ctorArgs.Select(Var))})"; + var props = obj.Properties.Where(p => !p.Ctor && ShouldInitializeInConstruction(p)) + .Select(p => $"{p.Name} = {Var(p)}").ToArray(); if (props.Length == 0) return ctor; return $"{ctor} {{ {string.Join(", ", props)} }}"; } @@ -126,14 +113,14 @@ private static string EmitFieldAccessor (SerializedObjectMeta obj, SerializedPro var value = obj.Clr.IsValueType ? $"ref {obj.Syntax} value" : $"{obj.Syntax} value"; return $""" [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<{prop.Name}>k__BackingField")] - private static extern ref {prop.Syntax} {prop.FieldAccessorName} ({value}); + private static extern ref {prop.Type.Syntax} {prop.FieldAccessor} ({value}); """; } private static string EmitFieldAssign (SerializedObjectMeta obj, SerializedPropertyMeta prop) { var value = obj.Clr.IsValueType ? "ref _value_" : "_value_"; - return $"{prop.FieldAccessorName}({value}) = {MangleLocal(prop.Name)};"; + return $"{prop.FieldAccessor}({value}) = {Var(prop)};"; } private static bool ShouldInitializeInConstruction (SerializedPropertyMeta prop) @@ -142,8 +129,8 @@ private static bool ShouldInitializeInConstruction (SerializedPropertyMeta prop) return prop.Required && prop.Kind == SerializedPropertyKind.Set; } - private static string MangleLocal (string name) + private static string Var (SerializedPropertyMeta prop) { - return $"@{ToFirstLower(name)}"; + return $"@{ToFirstLower(prop.Name)}"; } } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs new file mode 100644 index 00000000..bf4e616b --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -0,0 +1,166 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +internal sealed class DeclarationGenerator +{ + private readonly CodeBuilder bld = new(); + private readonly TypeSyntaxBuilder ts; + private readonly DocumentationBuilder doc; + private readonly SolutionInspection spec; + private readonly JSModules mds; + + public DeclarationGenerator (SolutionInspection spec, JSModules mds) + { + this.spec = spec; + this.mds = mds; + ts = new(mds); + doc = new(bld, spec.Docs); + } + + public string Generate (JSModule module) + { + bld.Clear(); + ts.EnterModule(module); + foreach (var node in module.Nodes) + DeclareNode(node); + return Fmt([EmitImports(module), bld.ToString()], 0, "\n\n"); + } + + private string EmitImports (JSModule md) => + $$""" + import type { Event } from "{{md.To("event")}}"; + {{Fmt(mds.GetImported(md).Select(imp => + $"""import type * as {imp.Alias} from "{md.ToGen(imp.Path)}";"""), 0)}} + """; + + private void DeclareNode (JSNode node) + { + var surf = node.Types.FirstOrDefault(s => s is SurfaceMeta and not InstanceMeta); + var wrap = surf != null || node.Children.Count > 0; + if (wrap) + { + 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 child in node.Children) + DeclareNode(child); + if (wrap) bld.Exit("}"); + } + + private void DeclareEnum (SerializedEnumMeta enu) + { + doc.Type(enu); + bld.Enter($$"""export enum {{ts.BuildName(enu.Clr)}} {"""); + var names = Enum.GetNames(enu.Clr); + for (var i = 0; i < names.Length; i++) + { + doc.Property(enu.Clr.GetField(names[i])!); + bld.Line(i == names.Length - 1 ? names[i] : $"{names[i]},"); + } + bld.Exit("}"); + } + + private void DeclareSerialized (SerializedObjectMeta obj) + { + doc.Type(obj); + var ext = spec.Types.HasBase(obj.Clr, out var bs) ? $"{ts.BuildFullName(bs.Clr)} & " : ""; + bld.Enter($$"""export type {{ts.BuildName(obj.Clr)}} = {{ext}}Readonly<{"""); + foreach (var prop in obj.Properties.Where(p => ShouldDeclareOn(obj.Clr, p.Info))) + { + doc.Property(prop.Info); + bld.Line($"{prop.JSName}{ts.BuildProperty(prop.Info)};"); + } + bld.Exit("}>;"); + } + + private void DeclareInstance (InstanceMeta it) + { + doc.Type(it); + bld.Enter($$"""export interface {{ts.BuildName(it.Clr)}}{{BuildExtensions()}} {"""); + foreach (var member in it.Members.Where(m => ShouldDeclareOn(it.Clr, m.Info))) + if (member is EventMeta evt) DeclareEvent(evt); + else if (member is PropertyMeta prop) DeclareProperty(prop); + else if (member is MethodMeta method) DeclareMethod(method); + bld.Exit("}"); + + string BuildExtensions () + { + var ext = it.Clr.GetInterfaces().Where(IsUserType).ToList(); + if (spec.Types.HasBase(it.Clr, out var bs)) ext.Insert(0, bs.Clr); + return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildFullName))}"; + } + + void DeclareEvent (EventMeta evt) + { + doc.Event(evt); + var args = string.Join(", ", evt.Args.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); + bld.Line($"{evt.JSName}: Event<[{args}]>;"); + } + + void DeclareProperty (PropertyMeta prop) + { + doc.Property(prop.Info); + var name = prop.CanSet ? prop.JSName : $"readonly {prop.JSName}"; + bld.Line($"{name}{ts.BuildProperty(prop.Info)};"); + } + + void DeclareMethod (MethodMeta method) + { + doc.Method(method); + var args = string.Join(", ", method.Args.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); + bld.Line($"{method.JSName}({args}): {ts.BuildReturn(method.Info)};"); + } + } + + private void DeclareSurface (SurfaceMeta surf) + { + foreach (var member in surf.Members) + if (member is EventMeta evt) DeclareEvent(evt); + else if (member is PropertyMeta prop) DeclareProperty(prop); + else if (member is MethodMeta method) DeclareMethod(method); + + void DeclareEvent (EventMeta evt) + { + doc.Event(evt); + var args = string.Join(", ", evt.Args.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); + bld.Line($"export const {evt.JSName}: Event<[{args}]>;"); + } + + void DeclareProperty (PropertyMeta prop) + { + doc.Property(prop.Info); + var stx = ts.BuildVariable(prop.Info); + var mod = prop.CanGet && !prop.CanSet ? "const" : "let"; + if (prop.IK == InteropKind.Import) + bld.Line($$"""export let {{prop.JSName}}: { {{Fmt([ + prop.CanGet ? $"get: () => {stx}" : null, + prop.CanSet ? $"set: (value: {stx}) => void" : null + ], 0, "; ")}} };"""); + else bld.Line($"export {mod} {prop.JSName}: {stx};"); + } + + void DeclareMethod (MethodMeta method) + { + doc.Method(method); + 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};"); + else bld.Line($"export let {method.JSName}: ({args}) => {result};"); + } + } + + private bool ShouldDeclareOn (Type host, MemberInfo member) + { + if (member.DeclaringType == host) return true; + return !spec.Types.HasBase(member.DeclaringType!, out _) && !spec.Types.HasBase(host, out _); + } +} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs similarity index 59% rename from src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs rename to src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs index 9f051d02..b9a4182d 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs @@ -1,29 +1,41 @@ using System.Reflection; -using System.Text; using System.Xml.Linq; namespace Bootsharp.Publish; -internal sealed class DocumentationBuilder (IReadOnlyCollection docs) +internal sealed class DocumentationBuilder { - public string BuildType (Type type, int indent) + private readonly Dictionary<(string Assembly, string Key), XElement> xmlByKey = []; + private readonly CodeBuilder bld; + + public DocumentationBuilder (CodeBuilder bld, IReadOnlyCollection docs) + { + this.bld = bld; + foreach (var doc in docs) + foreach (var member in doc.Xml.Descendants("member")) + if (member.Attribute("name") is { } name) + xmlByKey.TryAdd((doc.Assembly, name.Value), member); + } + + public void Type (TypeMeta type) { - var asm = type.Assembly.GetName().Name!; - var key = $"T:{GetXmlKey(type)}"; - return GetXml(asm, key) is { } xml ? Build(GetSummary(xml), indent) : ""; + var asm = type.Clr.Assembly.GetName().Name!; + var key = $"T:{GetXmlKey(type.Clr)}"; + if (GetXml(asm, key) is { } xml) + Append(GetSummary(xml)); } - public string BuildEvent (EventMeta evt, int indent) + public void Event (EventMeta evt) { var asm = evt.Info.DeclaringType!.Assembly.GetName().Name!; var key = $"E:{GetXmlKey(evt.Info.DeclaringType!)}.{evt.Name}"; - if (GetXml(asm, key) is not { } xml) return ""; + if (GetXml(asm, key) is not { } xml) return; var sum = GetSummary(xml); - foreach (var arg in evt.Arguments) + foreach (var arg in evt.Args) if ((GetArgXml(xml, arg) ?? GetDelegateArgXml(arg)) is { } x) sum.Add($"@param {arg.JSName} {x.Value}"); - return Build(sum, indent); + Append(sum); XElement? GetDelegateArgXml (ArgumentMeta arg) { @@ -33,36 +45,37 @@ public string BuildEvent (EventMeta evt, int indent) } } - public string BuildProperty (MemberInfo member, int indent) + public void Property (MemberInfo member) { var asm = member.DeclaringType!.Assembly.GetName().Name!; var key = $"{(member is FieldInfo ? "F" : "P")}:{GetXmlKey(member.DeclaringType!)}.{member.Name}"; - return GetXml(asm, key) is { } xml ? Build(GetSummary(xml), indent) : ""; + if (GetXml(asm, key) is { } xml) + Append(GetSummary(xml)); } - public string BuildFunction (MethodMeta method, int indent) + public void Method (MethodMeta method) { var asm = method.Info.DeclaringType!.Assembly.GetName().Name!; - if (GetXml(asm, GetMethodKey()) is not { } xml) return ""; + if (GetXml(asm, GetMethodKey(method)) is not { } xml) return; var sum = GetSummary(xml); - foreach (var arg in method.Arguments) + foreach (var arg in method.Args) if (GetArgXml(xml, arg) is { } x) sum.Add($"@param {arg.JSName} {x.Value}"); if (xml.Element("returns") is { } returns) sum.Add($"@returns {returns.Value}"); - return Build(sum, indent); + Append(sum); - string GetMethodKey () + static string GetMethodKey (MethodMeta method) { - var key = new StringBuilder($"M:{GetXmlKey(method.Info.DeclaringType!)}.{method.Name}"); + var bld = new CodeBuilder($"M:{GetXmlKey(method.Info.DeclaringType!)}.{method.Name}"); var args = method.Info.GetParameters(); if (args.Length > 0) - key.Append('(').AppendJoin(',', args.Select(p => GetArgKey(p.ParameterType))).Append(')'); - return key.ToString(); + bld.Append("(").Join(",", args.Select(p => GetArgKey(p.ParameterType))).Append(")"); + return bld.ToString(); } - string GetArgKey (Type type) + static string GetArgKey (Type type) { if (type.IsArray) return $"{GetArgKey(type.GetElementType()!)}[{new string(',', type.GetArrayRank() - 1)}]"; if (!type.IsGenericType) return GetXmlKey(type); @@ -73,15 +86,12 @@ string GetArgKey (Type type) } } - private string Build (IReadOnlyList summary, int indent) + private void Append (IReadOnlyList summary) { - var pad = new string(' ', indent * 4); - var builder = new StringBuilder(); - builder.Append($"\n{pad}/**"); + bld.Line("/**"); foreach (var line in summary) - builder.Append($"\n{pad} * {line}"); - builder.Append($"\n{pad} */"); - return builder.ToString(); + bld.Line($" * {line}"); + bld.Line(" */"); } private static string GetXmlKey (Type type) @@ -93,9 +103,7 @@ private static string GetXmlKey (Type type) private XElement? GetXml (string assembly, string key) { - return docs.Where(d => d.Assembly == assembly) - .SelectMany(d => d.Xml.Descendants("member")) - .FirstOrDefault(e => e.Attribute("name")!.Value == key); + return xmlByKey.GetValueOrDefault((assembly, key)); } private static XElement? GetArgXml (XElement xml, ArgumentMeta arg) @@ -103,7 +111,7 @@ private static string GetXmlKey (Type type) return xml.Elements("param").FirstOrDefault(e => e.Attribute("name")!.Value == arg.Info.Name); } - private List GetSummary (XElement xml) + private static List GetSummary (XElement xml) { return xml.Elements("summary").Select(e => e.Value.Trim()).ToList(); } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs similarity index 93% rename from src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs rename to src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs index 8940bf9d..8520476d 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs @@ -2,10 +2,16 @@ namespace Bootsharp.Publish; -internal sealed class TypeSyntaxBuilder (Preferences prefs) +internal sealed class TypeSyntaxBuilder (JSModules mds) { + private JSModule module = null!; private NullabilityInfo? nullity; + public void EnterModule (JSModule module) + { + this.module = module; + } + public string BuildName (Type type) { var full = BuildFullName(type); @@ -66,7 +72,7 @@ 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 WithPrefs(prefs.Type, type.FullName!, Build(type)); + return Build(type); } private string Build (Type type) @@ -119,13 +125,11 @@ private string BuildDictionary (Type key, Type value) private string BuildUser (Type type) { - var space = BuildJSSpace(type, prefs); - var name = TrimGeneric(type.Name); - var full = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; - if (!type.IsGenericType) return full; + var @ref = mds.Ref(type, module); + if (!type.IsGenericType) return @ref; EnterNullity(); var args = string.Join(", ", type.GetGenericArguments().Select(Build)); - return $"{full}<{args}>"; + return $"{@ref}<{args}>"; } private string BuildPrimitive (Type type) diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs b/src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs similarity index 97% rename from src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs rename to src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs index eb7f57e6..cd9d5d45 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/DotNetPatcher.cs @@ -4,7 +4,7 @@ namespace Bootsharp.Publish; -internal sealed class ModulePatcher (string buildDir) +internal sealed class DotNetPatcher (string buildDir) { private readonly string dotnet = Path.Combine(buildDir, "dotnet.js"); private readonly string runtime = Path.Combine(buildDir, "dotnet.runtime.js"); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/GenerateJS.cs b/src/cs/Bootsharp.Publish/GenerateJS/GenerateJS.cs new file mode 100644 index 00000000..cbc6e59d --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/GenerateJS.cs @@ -0,0 +1,103 @@ +namespace Bootsharp.Publish; + +/// +/// Second pass: emits JS sources and type declarations. +/// +public sealed class GenerateJS : Microsoft.Build.Utilities.Task +{ + public required string BuildDirectory { get; set; } + public required string DebugDirectory { get; set; } + public required string InspectedDirectory { get; set; } + public required string EntryAssemblyName { get; set; } + public required bool Globalization { get; set; } + public required bool LLVM { get; set; } + public required bool Debug { get; set; } + + public override bool Execute () + { + PreferencesResolver.Resolve(EntryAssemblyName, InspectedDirectory); + using var spec = InspectSolution(); + var mds = new JSModules(spec.Types); + GenerateImports(mds); + GenerateModules(mds); + GenerateSerializer(spec); + GenerateInstances(spec, mds); + GenerateDeclarations(spec, mds); + GenerateResources(spec); + PatchDotNet(); + return true; + } + + private SolutionInspection InspectSolution () + { + var inspector = new SolutionInspector(); + var inspection = inspector.Inspect(InspectedDirectory, GetFiles()); + new InspectionReporter(Log).Report(inspection); + return inspection; + + IEnumerable GetFiles () + { + if (LLVM) return Directory.GetFiles(InspectedDirectory, "*.dll").Order(); + // Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names). + // While the inspected dir contains extra assemblies we don't need in build. Hence the filtering. + var included = Directory.GetFiles(BuildDirectory, "*.wasm") + .Select(Path.GetFileNameWithoutExtension).ToHashSet(); + return Directory.GetFiles(InspectedDirectory, "*.dll").Order() + .Where(p => included.Contains(Path.GetFileNameWithoutExtension(p))); + } + } + + private void GenerateImports (JSModules mds) + { + var generator = new JSImportsGenerator(); + WriteGenerated("imports.g.mjs", generator.Generate(mds)); + } + + private void GenerateModules (JSModules mds) + { + var generator = new JSModuleGenerator(Debug); + foreach (var module in mds.List) + WriteGenerated($"{module.Path}.g.mjs", generator.Generate(module)); + } + + private void GenerateSerializer (SolutionInspection spec) + { + var generator = new JSSerializerGenerator(); + var serialized = spec.Types.OfType().ToArray(); + WriteGenerated("serializer.g.mjs", generator.Generate(serialized)); + } + + private void GenerateInstances (SolutionInspection spec, JSModules mds) + { + var generator = new JSInstanceGenerator(Debug, mds); + var instances = spec.Types.OfType().ToArray(); + WriteGenerated("instances.g.mjs", generator.Generate(instances)); + } + + private void GenerateDeclarations (SolutionInspection spec, JSModules mds) + { + var generator = new DeclarationGenerator(spec, mds); + foreach (var module in mds.List) + WriteGenerated($"{module.Path}.g.d.mts", generator.Generate(module)); + } + + private void GenerateResources (SolutionInspection spec) + { + var generator = new ResourceGenerator(EntryAssemblyName, Debug, Globalization); + var content = generator.Generate(BuildDirectory, DebugDirectory); + WriteGenerated("resources.g.mjs", content); + } + + private void PatchDotNet () + { + var patcher = new DotNetPatcher(BuildDirectory); + patcher.Patch(); + } + + private void WriteGenerated (string filename, string content) + { + var fullPath = Path.Combine(BuildDirectory, "generated", filename); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); + File.WriteAllText(fullPath, content); + } +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs new file mode 100644 index 00000000..2497811b --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSImportsGenerator.cs @@ -0,0 +1,19 @@ +namespace Bootsharp.Publish; + +internal sealed class JSImportsGenerator +{ + public string Generate (JSModules mds) => + $$""" + {{Fmt(mds.List.Select(EmitImport), 0)}} + + export function bindImports(runtime) { + {{Fmt(mds.List.Select(EmitBinding))}} + } + """; + + private string EmitImport (JSModule md) => + $"""import * as {md.Alias} from "./{md.Path}.g.mjs";"""; + + private string EmitBinding (JSModule md) => + $"""runtime.setModuleImports("{md.Path}", {md.Alias});"""; +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs new file mode 100644 index 00000000..e231cb67 --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSInstanceGenerator.cs @@ -0,0 +1,90 @@ +namespace Bootsharp.Publish; + +internal sealed class JSInstanceGenerator (bool debug, JSModules md) +{ + public string Generate (IReadOnlyCollection its) => + $$""" + import { Event } from "../event.mjs"; + import { {{(debug ? "exports, getExport" : "exports")}} } from "../exports.mjs"; + import { instances as $i } from "../instances.mjs"; + import $s, { serialize } from "./serializer.g.mjs"; + {{Fmt(EmitImports(), 0)}} + + {{Fmt([ + ..its.Where(s => s.Importer != null).Select(EmitImporter), + ..its.Where(s => s.IK == InteropKind.Export).Select(EmitProxy) + ], 0)}} + + export default $i; + """; + + private IEnumerable EmitImports () => md.List + .Where(m => m.Nodes.Any(o => o.Any(t => t is InstanceMeta { IK: InteropKind.Export }))) + .Select(m => $"""import * as {m.Alias} from "./{m.Path}.g.mjs";"""); + + private string EmitImporter (InstanceMeta it) + { + var evt = it.Members.OfType().ToArray(); + return + $$""" + $i.{{it.Importer}} = function (it) { + return $i.import(it, _id => { + {{Fmt(evt.Select(e => $"it.{e.JSName}.subscribe(handle{e.Name});"))}} + return () => { + {{Fmt(evt.Select(e => $"it.{e.JSName}.unsubscribe(handle{e.Name});"), 2)}} + }; + + {{Fmt(evt.Select(EmitHandler))}} + }); + }; + """; + + string EmitHandler (EventMeta e) + { + var fnName = $"{it.Proxy.Id}_Invoke{e.Name}"; + var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var args = string.Join(", ", e.Args.Select(a => a.JSName)); + var invArgs = PrependIdArg(string.Join(", ", e.Args.Select(ImportJS))); + return $"function handle{e.Name}({args}) {{ {invName}({invArgs}); }}"; + } + } + + private string EmitProxy (InstanceMeta it) => + $$""" + $i.{{it.Id}} = class {{it.Proxy.JS}} { + {{Fmt([ + "constructor(_id) { this._id = _id; }", + ..it.Members.Select(EmitMember) + ])}} + }; + """; + + private string EmitMember (MemberMeta member) => member switch { + EventMeta evt => EmitEvent(evt), + PropertyMeta prop => EmitProperty(prop), + _ => EmitMethod((MethodMeta)member) + }; + + private string EmitEvent (EventMeta evt) + { + var args = string.Join(", ", evt.Args.Select(a => a.JSName)); + return Fmt(0, + $"{evt.JSName} = new Event();", + $"broadcast{evt.Name}({args}) {{ this.{evt.JSName}.broadcast({args}); }}" + ); + } + + 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 bodyExp = $"{md.Ref(method.Surf)}.{method.JSName}({invArgs})"; + if (!method.Void) bodyExp = $"return {bodyExp}"; + return $"{method.JSName}({sigArgs}) {{ {bodyExp}; }}"; + } + + private string EmitProperty (PropertyMeta p) => Fmt(0, + p.CanGet ? $"get {p.JSName}() {{ return {md.Ref(p.Surf)}.get{p.Name}(this._id); }}" : null, + p.CanSet ? $"set {p.JSName}(value) {{ {md.Ref(p.Surf)}.set{p.Name}(this._id, value); }}" : null + ); +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs new file mode 100644 index 00000000..c30de898 --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs @@ -0,0 +1,190 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Bootsharp.Publish; + +internal sealed class JSModuleGenerator (bool debug) +{ + private readonly CodeBuilder bld = new(); + + [MemberNotNullWhen(true, nameof(it))] + private bool isIt => srf is InstanceMeta; + + private string id = null!; + private SurfaceMeta srf = null!; + private InstanceMeta? it => srf as InstanceMeta; + + public string Generate (JSModule module) + { + bld.Clear(); + foreach (var node in module.Nodes) + EmitNode(node, $"export const {node.Name} = {{", "};"); + return Fmt([EmitImports(module), bld.ToString()], 0, "\n\n"); + } + + private string EmitImports (JSModule md) => + $$""" + import { Event } from "{{md.To("event")}}"; + import { {{(debug ? "exports, getExport" : "exports")}} } from "{{md.To("exports")}}"; + import { {{(debug ? "importEvent, getImport" : "importEvent")}} } from "{{md.To("imports")}}"; + import $i from "{{md.ToGen("instances")}}"; + import $s, { serialize, deserialize } from "{{md.ToGen("serializer")}}"; + """; + + private void EmitNode (JSNode node, string header, string footer) + { + if (!node.Any(t => t is SurfaceMeta || t is SerializedEnumMeta)) return; + bld.Enter(header, ","); + foreach (var type in node.Types) + if (type is SerializedEnumMeta enu) + EmitEnum(enu); + else if (type is SurfaceMeta srf) + foreach (var member in srf.Members) + EmitMember(member, srf); + foreach (var child in node.Children) + EmitNode(child, $"{child.Name}: {{", "}"); + bld.Exit(footer); + } + + private void EmitEnum (SerializedEnumMeta enu) + { + var values = Enum.GetValuesAsUnderlyingType(enu.Clr).Cast().ToArray(); + foreach (var value in values) + bld.Line($"\"{value}\": \"{Enum.GetName(enu.Clr, value)}\""); + foreach (var value in values) + bld.Line($"\"{Enum.GetName(enu.Clr, value)}\": {value}"); + } + + private void EmitMember (MemberMeta member, SurfaceMeta surf) + { + srf = surf; + id = (surf as ProxyMeta)?.Proxy.Id ?? surf.Id; + switch (member) + { + case EventMeta { IK: InteropKind.Export } e: EmitEventExport(e); break; + case EventMeta { IK: InteropKind.Import } e: EmitEventImport(e); break; + case PropertyMeta { IK: InteropKind.Export } p: EmitPropertyExport(p); break; + case PropertyMeta { IK: InteropKind.Import } p: EmitPropertyImport(p); break; + case MethodMeta { IK: InteropKind.Export } m: EmitMethodExport(m); break; + default: EmitMethodImport((MethodMeta)member); break; + } + } + + private void EmitEventExport (EventMeta evt) + { + var name = $"broadcast{evt.Name}Serialized"; + var args = string.Join(", ", evt.Args.Select(a => a.JSName)); + var invArgs = string.Join(", ", evt.Args.Select(arg => + // By default, we use 'null' for missing collection items, but here the event args array + // represents args specified to the event's 'broadcast' function, so user expects 'undefined'. + $"{ExportJS(arg)}{(arg.Value.Nullable ? " ?? undefined" : "")}")); + if (isIt) + { + var invName = $"$i.resolve(_id, $i.{it.Id}).broadcast{evt.Name}"; + bld.Line($"{name}: ({PrependIdArg(args)}) => {invName}({invArgs})"); + } + else + { + var invName = $"{srf.JSNode}.{evt.JSName}.broadcast"; + bld.Line($"{evt.JSName}: new Event()"); + bld.Line($"{name}: ({args}) => {invName}({invArgs})"); + } + } + + private void EmitEventImport (EventMeta evt) + { + if (isIt) return; // instance import events handled in instances.g.mjs + var fnName = $"{id}_Invoke{evt.Name}"; + var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var args = string.Join(", ", evt.Args.Select(a => a.JSName)); + var invArgs = string.Join(", ", evt.Args.Select(ImportJS)); + bld.Line($"{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); + } + + private void EmitPropertyExport (PropertyMeta prop) + { + if (prop.CanGet) + { + 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"; + bld.Line(isIt + ? $"get{prop.Name}(_id) {{ return {body}; }}" + : $"get {prop.JSName}() {{ return {body}; }}"); + } + if (prop.CanSet) + { + var fnName = $"{id}_Set{prop.Name}"; + var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var value = ImportJS(prop.Set, "value"); + var call = isIt ? $"{invName}(_id, {value})" : $"{invName}({value})"; + bld.Line(isIt + ? $"set{prop.Name}(_id, value) {{ {call}; }}" + : $"set {prop.JSName}(value) {{ {call}; }}"); + } + } + + private void EmitPropertyImport (PropertyMeta prop) + { + if (prop.CanGet) + { + var exp = ImportJS(prop.Get, isIt + ? $"$i.imported(_id).{prop.JSName}" + : $"this.{prop.JSName}.get()"); + var argsList = isIt ? "_id" : ""; + bld.Line($"get{prop.Name}Serialized({argsList}) {{ return {exp}; }}"); + } + if (prop.CanSet) + { + var value = ExportJS(prop.Set, "value"); + var argsList = isIt ? "_id, value" : "value"; + var bodyExp = isIt + ? $"$i.imported(_id).{prop.JSName} = {value}" + : $"this.{prop.JSName}.set({value})"; + bld.Line($"set{prop.Name}Serialized({argsList}) {{ {bodyExp}; }}"); + } + } + + private void EmitMethodExport (MethodMeta method) + { + var wait = ShouldWait(method); + var fnName = $"{id}_{method.Name}"; + var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var args = string.Join(", ", method.Args.Select(a => a.JSName)); + if (isIt) args = PrependIdArg(args); + var invArgs = string.Join(", ", method.Args.Select(ImportJS)); + if (isIt) invArgs = PrependIdArg(invArgs); + var bodyExp = ExportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + bld.Line($"{method.JSName}: {(wait ? "async " : "")}({args}) => {bodyExp}"); + } + + private void EmitMethodImport (MethodMeta method) + { + var wait = ShouldWait(method); + var name = method.JSName; + 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 bodyExp = ImportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + var srdHandler = $"{(wait ? "async " : "")}({args}) => {bodyExp}"; + if (isIt) bld.Line($"{name}Serialized: {srdHandler}"); + else + { + var srd = $"this.{name}SerializedHandler"; + var srdExp = debug ? $"getImport({invName}, {srd}, \"{srf.JSNode}.{name}\")" : srd; + bld.Line($"get {name}() {{ return {invName}; }}"); + bld.Line($"set {name}(handler) {{ {invName} = handler; {srd} = {srdHandler}; }}"); + bld.Line($"get {name}Serialized() {{ return {srdExp}; }}"); + } + } + + private bool ShouldWait (MethodMeta method) + { + if (!method.Async) return false; + return method.Args.Any(a => a.Value.IsSerialized || a.Value.IsInstanced) || + method.Return.IsSerialized || method.Return.IsInstanced; + } +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModule.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModule.cs new file mode 100644 index 00000000..8ee72d5b --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModule.cs @@ -0,0 +1,52 @@ +namespace Bootsharp.Publish; + +/// +/// A JavaScript module projecting an interop surface under a namespace. +/// +internal sealed record JSModule +{ + /// + /// Path to the module file, without file extension. + /// + public string Path { get; } + /// + /// Alias used when importing and referencing the module in other modules. + /// + public string Alias { get; } + /// + /// The graph of the JavaScript nodes declared under the module. + /// + public IReadOnlyCollection Nodes { get; } + + private readonly string root, gen; + + public JSModule (string path, IReadOnlyList types) + { + Path = path; + Alias = path.Replace('/', '_').Replace('-', '_'); + Nodes = Graph(types.Select(type => (type, path: type.JSNode))); + var depth = path.Count(c => c == '/') + 1; + root = string.Concat(Enumerable.Repeat("../", depth)); + gen = depth == 1 ? "./" : string.Concat(Enumerable.Repeat("../", depth - 1)); + } + + /// + /// Builds relative path from this module to a "mjs" file with the specified name + /// stored under the package's root directory. + /// + public string To (string filename) => $"{root}{filename}.mjs"; + /// + /// Builds relative path from this module to a "g.mjs" file with the specified name + /// stored under the package's 'generated' directory. + /// + public string ToGen (string filename) => $"{gen}{filename}.g.mjs"; + + private static IReadOnlyList Graph (IEnumerable<(TypeMeta type, string path)> types) => types + .GroupBy(o => o.path.Split('.', 2)[0]) + .Select(g => new JSNode { + Name = g.Key, + Types = g.Where(x => x.path == g.Key).Select(x => x.type).ToArray(), + Children = Graph(g.Where(x => x.path != g.Key).Select(x => (x.type, x.path[(g.Key.Length + 1)..]))).ToArray() + }) + .ToArray(); +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs new file mode 100644 index 00000000..a07f4216 --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs @@ -0,0 +1,57 @@ +namespace Bootsharp.Publish; + +/// +/// The collection projecting solution interop surface. +/// +internal sealed class JSModules +{ + /// + /// All the JavaScript modules in the solution. + /// + public IReadOnlyCollection List { get; } + + private readonly IReadOnlyCollection types; + private readonly Dictionary mdByPath; + private readonly Dictionary> importedByMd = []; + + public JSModules (IReadOnlyCollection types) + { + List = types.GroupBy(t => t.JSModule).Select(g => new JSModule(g.Key, g.ToArray())).ToArray(); + mdByPath = List.ToDictionary(m => m.Path); + this.types = types; + } + + /// + /// Returns fully qualified node reference to the specified type. + /// When is specified, will track the import. + /// + public string Ref (TypeMeta type, JSModule? fromMd = null) + { + var toMd = mdByPath[type.JSModule]; + if (fromMd == toMd) return type.JSNode; + if (fromMd != null) Import(fromMd, toMd); + return $"{toMd.Alias}.{type.JSNode}"; + } + + /// + public string Ref (Type clr, JSModule? fromMd = null) + { + return Ref(types.Get(clr), fromMd); + } + + /// + /// Returns all modules imported via from the specified module. + /// + public IReadOnlyCollection GetImported (JSModule fromMd) + { + return importedByMd.TryGetValue(fromMd, out var importedByPath) + ? importedByPath.Values.ToArray() : []; + } + + private void Import (JSModule from, JSModule to) + { + if (!importedByMd.TryGetValue(from, out var importedByPath)) + importedByMd[from] = importedByPath = []; + importedByPath[to.Path] = to; + } +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSNode.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSNode.cs new file mode 100644 index 00000000..8b26065b --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSNode.cs @@ -0,0 +1,43 @@ +namespace Bootsharp.Publish; + +/// +/// A JavaScript node declared under a . +/// +/// +/// Nodes are standalone JavaScript or TypeScript artifacts declared under a module, +/// and include objects, enums and namespaces. +/// +internal sealed record JSNode +{ + /// + /// Name of the node. + /// + public required string Name { get; init; } + /// + /// Types represented by the node. + /// + /// + /// In most cases types are mapped 1:1 to JS nodes, with the following exceptions: + /// 1. Two sides of a bidirectional (import+export) instance surface, both end up in the same node. + /// 2. User prefs collapsing unique type names into one, all end in the same node. + /// + public required IReadOnlyList Types { get; init; } + /// + /// The node's children. + /// + public required IReadOnlyList Children { get; init; } + + /// + /// Whether any type under the node or any of the child nodes satisfies the predicate. + /// + public bool Any (Predicate filter) + { + foreach (var type in Types) + if (filter(type)) + return true; + foreach (var node in Children) + if (node.Any(filter)) + return true; + return false; + } +} diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs new file mode 100644 index 00000000..e556b48a --- /dev/null +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSSerializerGenerator.cs @@ -0,0 +1,84 @@ +namespace Bootsharp.Publish; + +internal sealed class JSSerializerGenerator +{ + public string Generate (IReadOnlyCollection srd) => + $$""" + import $i from "./instances.g.mjs"; + import $s from "../serialization/index.mjs"; + + export const { serialize, deserialize } = $s; + + {{Fmt(srd.Select(EmitFactory), 0)}} + + {{Fmt(srd.OfType().Select(EmitInstanced), 0, "\n\n")}} + + {{Fmt(srd.OfType().Select(EmitObject), 0, "\n\n")}} + + export default $s; + """; + + private string EmitFactory (SerializedMeta meta) => + $"$s.{meta.Id} = {meta switch { + SerializedEnumMeta => "$s.std.Int32", + SerializedNullableMeta nullable => $"$s.std.Nullable($s.{nullable.Value.Id})", + SerializedArrayMeta arr => $"$s.std.Array($s.{arr.Element.Id})", + SerializedListMeta list => $"$s.std.List($s.{list.Element.Id})", + SerializedDictionaryMeta dic => $"$s.std.Dictionary($s.{dic.Key.Id}, $s.{dic.Value.Id})", + SerializedObjectMeta or SerializedInstanceMeta => $"$s.binary(write_{meta.Id}, read_{meta.Id})", + _ => ResolvePrimitive(meta.Clr) + }};"; + + private static string ResolvePrimitive (Type type) => + type.FullName == typeof(nint).FullName ? "$s.std.IntPtr" : + type.FullName == typeof(DateTimeOffset).FullName ? "$s.std.DateTimeOffset" : + $"$s.std.{Type.GetTypeCode(type)}"; + + private string EmitInstanced (SerializedInstanceMeta it) => + $$""" + function write_{{it.Id}}(writer, value) { + writer.writeInt32({{ImportJS(it.Instance, "value")}}); + } + + function read_{{it.Id}}(reader) { + return {{ExportJS(it.Instance, "reader.readInt32()")}}; + } + """; + + private string EmitObject (SerializedObjectMeta obj) => + $$""" + function write_{{obj.Id}}(writer, value) { + {{Fmt(EmitObjectWrite(obj))}} + } + + function read_{{obj.Id}}(reader) { + {{Fmt(EmitObjectRead(obj))}} + } + """; + + private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) + { + if (!obj.Clr.IsValueType) + { + yield return "writer.writeBool(value != null);"; + yield return "if (value == null) return;"; + } + foreach (var p in obj.Properties) + if (p.Nullable) + { + yield return $"writer.writeBool(value.{p.JSName} != null);"; + yield return $"if (value.{p.JSName} != null) $s.{p.Type.Id}.write(writer, value.{p.JSName});"; + } + else yield return $"$s.{p.Type.Id}.write(writer, value.{p.JSName});"; + } + + private IEnumerable EmitObjectRead (SerializedObjectMeta obj) + { + if (!obj.Clr.IsValueType) yield return "if (!reader.readBool()) return null;"; + yield return "const value = {};"; + foreach (var p in obj.Properties) + if (p.Nullable) yield return $"if (reader.readBool()) value.{p.JSName} = $s.{p.Type.Id}.read(reader);"; + else yield return $"value.{p.JSName} = $s.{p.Type.Id}.read(reader);"; + yield return "return value;"; + } +} diff --git a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs rename to src/cs/Bootsharp.Publish/GenerateJS/ResourceGenerator.cs diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs deleted file mode 100644 index 28ca63b1..00000000 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Bootsharp.Publish; - -internal sealed class BindingClassGenerator -{ - public string Generate (SolutionInspection spec) - { - var exported = spec.Instanced.Where(i => i.Interop == InteropKind.Export); - return Fmt(exported.Select(EmitClass), 0) + '\n'; - } - - private string EmitClass (InstancedMeta instance) => - $$""" - class {{instance.JSName}} { - {{Fmt([ - "constructor(_id) { this._id = _id; }", - ..instance.Members.Select(EmitMember) - ])}} - } - """; - - private string EmitMember (MemberMeta member) => member switch { - EventMeta evt => EmitEvent(evt), - PropertyMeta prop => EmitProperty(prop), - _ => EmitMethod((MethodMeta)member) - }; - - private string EmitEvent (EventMeta evt) - { - var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); - return Fmt(0, - $"{evt.JSName} = new Event();", - $"broadcast{evt.Name}({args}) {{ this.{evt.JSName}.broadcast({args}); }}" - ); - } - - private string EmitMethod (MethodMeta method) - { - var sigArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); - var invArgs = sigArgs.Length > 0 ? $"this._id, {sigArgs}" : "this._id"; - var body = $"{method.JSSpace}.{method.JSName}({invArgs})"; - if (!method.Void) body = $"return {body}"; - return $"{method.JSName}({sigArgs}) {{ {body}; }}"; - } - - private string EmitProperty (PropertyMeta p) => Fmt(0, - p.CanGet ? $"get {p.JSName}() {{ return {p.JSSpace}.getProperty{p.Name}(this._id); }}" : null, - p.CanSet ? $"set {p.JSName}(value) {{ {p.JSSpace}.setProperty{p.Name}(this._id, value); }}" : null - ); -} diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs deleted file mode 100644 index 32bb7344..00000000 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ /dev/null @@ -1,328 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Bootsharp.Publish; - -internal sealed class BindingGenerator (Preferences prefs, bool debug) -{ - private record Binding (MemberMeta? Member, Type? Enum, InstancedMeta? It, string Id, string Space); - - private Binding binding => bindings[index]; - private Binding? prevBinding => index == 0 ? null : bindings[index - 1]; - private Binding? nextBinding => index == bindings.Length - 1 ? null : bindings[index + 1]; - [MemberNotNullWhen(true, nameof(it))] - private bool isIt => it != null; - private InstancedMeta? it => binding.It; - private string space => binding.Space; - private string id => binding.Id; - - private readonly StringBuilder bld = new(); - private Binding[] bindings = []; - private int index, level; - - public string Generate (SolutionInspection spec) - { - bindings = spec.Static - .Select(m => new Binding(m, null, null, m.Space.Replace('.', '_'), m.JSSpace)) - .Concat(spec.Modules.SelectMany(md => md.Members - .Select(m => new Binding(m, null, null, md.FullName.Replace('.', '_'), m.JSSpace)))) - .Concat(spec.Instanced.SelectMany(it => it.Members - .Select(m => new Binding(m, null, it, it.FullName.Replace('.', '_'), m.JSSpace)))) - .Concat(spec.Serialized.Where(t => t.Clr.IsEnum) - .Select(t => new Binding(null, t.Clr, null, "", BuildJSSpace(t.Clr, prefs)))) - .OrderBy(m => m.Space).ToArray(); - if (bindings.Length == 0) return ""; - - EmitImports(); - bld.Append("\n\n"); - - if (debug) - { - EmitDebugHelpers(); - bld.Append("\n\n"); - } - - EmitHelpers(); - bld.Append("\n\n"); - - bld.Append(new BindingSerializerGenerator().Generate(spec.Serialized)); - bld.Append("\n\n"); - - foreach (var it in spec.Instanced.Where(i => i.Importer != null)) - EmitImporter(it); - bld.Append("\n\n"); - - if (spec.Instanced.Count > 0) - bld.Append(new BindingClassGenerator().Generate(spec)); - - for (index = 0; index < bindings.Length; index++) - EmitBinding(); - - return bld.ToString(); - } - - private void EmitImports () - { - bld.Append( - """ - import { exports } from "../exports.mjs"; - import { Event } from "../event.mjs"; - import { instances } from "../instances.mjs"; - import { serialize, deserialize, binary, types } from "../serialization.mjs"; - """ - ); - } - - private void EmitDebugHelpers () - { - bld.Append( - """ - function getExport(name) { - return (...args) => { - if (exports == null) throw Error("Boot the runtime before invoking C# APIs."); - let result; - try { result = exports[name](...args); } - catch (error) { throw Error(`${error.message}\n${error.stack}`); } - if (typeof result?.then === "function") - return result.catch(error => { throw Error(`${error.message}\n${error.stack}`); }); - return result; - }; - } - - function getImport(handler, serializedHandler, name) { - if (typeof handler !== "function") throw Error(`Failed to invoke '${name}' from C#. Make sure to assign the function in JavaScript.`); - return serializedHandler; - } - """ - ); - } - - private void EmitHelpers () - { - bld.Append( - """ - function importEvent(handler) { - const event = new Event(); - const broadcast = event.broadcast.bind(event); - event.broadcast = (...args) => { broadcast(...args); handler(...args); }; - return event; - } - """ - ); - } - - private void EmitBinding () - { - if (ShouldOpenNamespace()) OpenNamespace(); - if (binding.Member != null) EmitMember(binding.Member); - else EmitEnum(binding.Enum!); - if (ShouldCloseNamespace()) CloseNamespace(); - } - - private bool ShouldOpenNamespace () - { - if (prevBinding is null) return true; - return prevBinding.Space != binding.Space; - } - - private void OpenNamespace () - { - level = 0; - var prevParts = prevBinding?.Space.Split('.') ?? []; - var parts = binding.Space.Split('.'); - while (prevParts.ElementAtOrDefault(level) == parts[level]) level++; - for (var i = level; i < parts.Length; level = i, i++) - if (i == 0) bld.Append($"\nexport const {parts[i]} = {{"); - else bld.Append($"{Comma}\n{Pad(i)}{parts[i]}: {{"); - } - - private bool ShouldCloseNamespace () - { - if (nextBinding is null) return true; - return nextBinding.Space != binding.Space; - } - - private void CloseNamespace () - { - var target = GetCloseLevel(); - for (; level >= target; level--) - if (level == 0) bld.Append("\n};"); - else bld.Append($"\n{Pad(level)}}}"); - - int GetCloseLevel () - { - if (nextBinding is null) return 0; - var closeLevel = 0; - var parts = binding.Space.Split('.'); - var nextParts = nextBinding.Space.Split('.'); - for (var i = 0; i < parts.Length; i++) - if (parts[i] == nextParts[i]) closeLevel++; - else break; - return closeLevel; - } - } - - private void EmitMember (MemberMeta member) - { - switch (member) - { - case EventMeta { Interop: InteropKind.Export } e: EmitEventExport(e); break; - case EventMeta { Interop: InteropKind.Import } e: EmitEventImport(e); break; - case PropertyMeta { Interop: InteropKind.Export } p: EmitPropertyExport(p); break; - case PropertyMeta { Interop: InteropKind.Import } p: EmitPropertyImport(p); break; - case MethodMeta { Interop: InteropKind.Export } m: EmitMethodExport(m); break; - case MethodMeta { Interop: InteropKind.Import } m: EmitMethodImport(m); break; - } - } - - private void EmitEventExport (EventMeta evt) - { - var name = $"broadcast{evt.Name}Serialized"; - var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); - var invArgs = string.Join(", ", evt.Arguments.Select(arg => - // By default, we use 'null' for missing collection items, but here the event args array - // represents args specified to the event's 'broadcast' function, so user expects 'undefined'. - $"{ImportJS(arg)}{(arg.Value.Nullable ? " ?? undefined" : "")}")); - if (isIt) - { - var invName = $"instances.export(_id, id => new {it.JSName}(id)).broadcast{evt.Name}"; - bld.Append($"{Br}{name}: ({PrependIdArg(args)}) => {invName}({invArgs})" - .IgnoreV8("id =>")); // Uncoverable, as finalization in Node is not controllable. - } - else - { - var invName = $"{evt.JSSpace}.{evt.JSName}.broadcast"; - bld.Append($"{Br}{evt.JSName}: new Event()"); - bld.Append($"{Br}{name}: ({args}) => {invName}({invArgs})"); - } - } - - private void EmitEventImport (EventMeta evt) - { - if (isIt) return; // instanced import event handlers are emitted in the registrar - var name = $"{id}_Invoke{evt.Name}"; - var invName = debug ? $"""getExport("{name}")""" : $"exports.{name}"; - var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); - var invArgs = string.Join(", ", evt.Arguments.Select(ExportJS)); - bld.Append($"{Br}{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); - } - - private void EmitPropertyExport (PropertyMeta prop) - { - if (prop.CanGet) - { - var fnName = $"{id}_GetProperty{prop.Name}"; - var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var body = ImportJS(prop.GetValue, isIt ? $"{invName}(_id)" : $"{invName}()"); - if (prop.GetValue.Nullable && !prop.GetValue.IsInstanced) body += " ?? undefined"; - if (isIt) bld.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); - else bld.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); - } - if (prop.CanSet) - { - var fnName = $"{id}_SetProperty{prop.Name}"; - var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var value = ExportJS(prop.SetValue, "value"); - var body = isIt ? $"{invName}(_id, {value})" : $"{invName}({value})"; - if (isIt) bld.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); - else bld.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); - } - } - - private void EmitPropertyImport (PropertyMeta prop) - { - if (prop.CanGet) - { - var args = isIt ? "_id" : ""; - var body = ExportJS(prop.GetValue, - isIt ? $"instances.imported(_id).{prop.JSName}" : $"this.{prop.JSName}.get()"); - bld.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); - } - if (prop.CanSet) - { - var value = ImportJS(prop.SetValue, "value"); - var args = isIt ? "_id, value" : "value"; - var body = isIt ? $"instances.imported(_id).{prop.JSName} = {value}" : $"this.{prop.JSName}.set({value})"; - bld.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); - } - } - - private void EmitMethodExport (MethodMeta method) - { - var wait = ShouldWait(method); - var fnName = $"{id}_{method.Name}"; - var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (isIt) args = PrependIdArg(args); - var invArgs = string.Join(", ", method.Arguments.Select(ExportJS)); - if (isIt) invArgs = PrependIdArg(invArgs); - var body = ImportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); - bld.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({args}) => {body}"); - } - - private void EmitMethodImport (MethodMeta method) - { - var wait = ShouldWait(method); - var name = method.JSName; - var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (isIt) args = PrependIdArg(args); - var invArgs = string.Join(", ", method.Arguments.Select(ImportJS)); - var invName = isIt ? $"instances.imported(_id).{name}" : $"this.{name}Handler"; - var body = ExportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); - var serdeHandler = $"{(wait ? "async " : "")}({args}) => {body}"; - if (isIt) bld.Append($"{Br}{name}Serialized: {serdeHandler}"); - else - { - var serde = $"this.{name}SerializedHandler"; - var serdeExp = debug ? $"getImport({invName}, {serde}, \"{space}.{name}\")" : serde; - bld.Append($"{Br}get {name}() {{ return {invName}; }}"); - bld.Append($"{Br}set {name}(handler) {{ {invName} = handler; {serde} = {serdeHandler}; }}"); - bld.Append($"{Br}get {name}Serialized() {{ return {serdeExp}; }}"); - } - } - - private void EmitEnum (Type @enum) - { - var values = Enum.GetValuesAsUnderlyingType(@enum).Cast().ToArray(); - var fields = string.Join(", ", values - .Select(v => $"\"{v}\": \"{Enum.GetName(@enum, v)}\"") - .Concat(values.Select(v => $"\"{Enum.GetName(@enum, v)}\": {v}"))); - bld.Append($"{Br}{@enum.Name}: {{ {fields} }}"); - } - - private void EmitImporter (InstancedMeta it) - { - var evt = it.Members.OfType().ToArray(); - bld.Append( - $$""" - function {{it.Importer}}(instance) { - return instances.import(instance, _id => { - {{Fmt(evt.Select(e => $"instance.{e.JSName}.subscribe(handle{e.Name});"))}} - return () => { - {{Fmt(evt.Select(e => $"instance.{e.JSName}.unsubscribe(handle{e.Name});"), 2)}} - }; - - {{Fmt(evt.Select(e => { - var fnName = $"{it.FullName.Replace('.', '_')}_Invoke{e.Name}"; - var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var args = string.Join(", ", e.Arguments.Select(a => a.JSName)); - var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(ExportJS))); - return $"function handle{e.Name}({args}) {{ {invName}({invArgs}); }}"; - }))}} - }); - } - """ - ); - } - - private bool ShouldWait (MethodMeta method) - { - if (!method.Async) return false; - return method.Arguments.Any(a => a.Value.IsSerialized || a.Value.IsInstanced) || - method.Return.IsSerialized || method.Return.IsInstanced; - } - - private string Br => $"{Comma}\n{Pad(level + 1)}"; - private string Pad (int level) => new(' ', level * 4); - private string Comma => bld[^1] == '{' ? "" : ","; -} diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs deleted file mode 100644 index 867f40fb..00000000 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Bootsharp.Publish; - -internal sealed class BindingSerializerGenerator -{ - public string Generate (IReadOnlyCollection serialized) - { - if (serialized.Count == 0) return ""; - return $""" - {Fmt(serialized.Select(EmitFactory), 0)} - - {Fmt(serialized.SelectMany(EmitHelpers), 0, "\n\n")} - """; - } - - private string EmitFactory (SerializedMeta meta) - { - return $"const {meta.Id} = {meta switch { - SerializedEnumMeta => "types.Int32", - SerializedNullableMeta nullable => $"types.Nullable({nullable.Value.Id})", - SerializedArrayMeta arr => $"types.Array({arr.Element.Id})", - SerializedListMeta list => $"types.List({list.Element.Id})", - SerializedDictionaryMeta dic => $"types.Dictionary({dic.Key.Id}, {dic.Value.Id})", - SerializedObjectMeta or SerializedInstanceMeta => $"binary(write_{meta.Id}, read_{meta.Id})", - _ => ResolvePrimitive(meta.Clr) - }};"; - - static string ResolvePrimitive (Type type) - { - if (type.FullName == typeof(nint).FullName) return "types.IntPtr"; - if (type.FullName == typeof(DateTimeOffset).FullName) return "types.DateTimeOffset"; - return $"types.{Type.GetTypeCode(type)}"; - } - } - - private IEnumerable EmitHelpers (SerializedMeta meta) - { - if (meta is SerializedInstanceMeta it) - { - yield return - $$""" - function write_{{it.Id}}(writer, value) { - writer.writeInt32({{ExportJS(it.Instance, "value")}}); - } - - function read_{{it.Id}}(reader) { - return {{ImportJS(it.Instance, "reader.readInt32()")}}; - } - """; - } - if (meta is SerializedObjectMeta obj) - { - yield return - $$""" - function write_{{obj.Id}}(writer, value) { - {{Fmt(EmitObjectWrite(obj))}} - } - - function read_{{obj.Id}}(reader) { - {{Fmt(EmitObjectRead(obj))}} - } - """; - } - } - - private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) - { - if (!obj.Clr.IsValueType) - { - yield return "writer.writeBool(value != null);"; - yield return "if (value == null) return;"; - } - foreach (var p in obj.Properties) - if (p.OmitWhenNull) - { - yield return $"writer.writeBool(value.{p.JSName} != null);"; - yield return $"if (value.{p.JSName} != null) {p.Id}.write(writer, value.{p.JSName});"; - } - else yield return $"{p.Id}.write(writer, value.{p.JSName});"; - } - - private IEnumerable EmitObjectRead (SerializedObjectMeta obj) - { - if (!obj.Clr.IsValueType) yield return "if (!reader.readBool()) return null;"; - yield return "const value = {};"; - foreach (var p in obj.Properties) - if (p.OmitWhenNull) yield return $"if (reader.readBool()) value.{p.JSName} = {p.Id}.read(reader);"; - else yield return $"value.{p.JSName} = {p.Id}.read(reader);"; - yield return "return value;"; - } -} diff --git a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs deleted file mode 100644 index 957a039b..00000000 --- a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Second pass: emits JS bindings and type declarations, bundles ES module. -/// -public sealed class BootsharpPack : Microsoft.Build.Utilities.Task -{ - public required string BuildDirectory { get; set; } - public required string DebugDirectory { get; set; } - public required string InspectedDirectory { get; set; } - public required string EntryAssemblyName { get; set; } - public required bool Globalization { get; set; } - public required bool LLVM { get; set; } - public required bool Debug { get; set; } - - public override bool Execute () - { - var prefs = ResolvePreferences(); - using var inspection = InspectSolution(prefs); - GenerateBindings(prefs, inspection); - GenerateDeclarations(prefs, inspection); - GenerateResources(inspection); - PatchModules(); - return true; - } - - private Preferences ResolvePreferences () - { - var resolver = new PreferencesResolver(EntryAssemblyName); - return resolver.Resolve(InspectedDirectory); - } - - private SolutionInspection InspectSolution (Preferences prefs) - { - var inspector = new SolutionInspector(prefs); - var inspection = inspector.Inspect(InspectedDirectory, GetFiles()); - new InspectionReporter(Log).Report(inspection); - return inspection; - - IEnumerable GetFiles () - { - if (LLVM) return Directory.GetFiles(InspectedDirectory, "*.dll").Order(); - // Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names). - // While the inspected dir contains extra assemblies we don't need in build. Hence the filtering. - var included = Directory.GetFiles(BuildDirectory, "*.wasm") - .Select(Path.GetFileNameWithoutExtension).ToHashSet(); - return Directory.GetFiles(InspectedDirectory, "*.dll").Order() - .Where(p => included.Contains(Path.GetFileNameWithoutExtension(p))); - } - } - - private void GenerateBindings (Preferences prefs, SolutionInspection spec) - { - var generator = new BindingGenerator(prefs, Debug); - var content = generator.Generate(spec); - WriteGenerated("bindings.g.mjs", content); - } - - private void GenerateDeclarations (Preferences prefs, SolutionInspection spec) - { - var generator = new DeclarationGenerator(prefs); - var content = generator.Generate(spec); - WriteGenerated("bindings.g.d.mts", content); - } - - private void GenerateResources (SolutionInspection spec) - { - var generator = new ResourceGenerator(EntryAssemblyName, Debug, Globalization); - var content = generator.Generate(BuildDirectory, DebugDirectory); - WriteGenerated("resources.g.mjs", content); - } - - private void PatchModules () - { - var patcher = new ModulePatcher(BuildDirectory); - patcher.Patch(); - } - - private void WriteGenerated (string filename, string content) - { - var dir = Path.Combine(BuildDirectory, "generated"); - Directory.CreateDirectory(dir); - File.WriteAllText(Path.Combine(dir, filename), content); - } -} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs deleted file mode 100644 index 3595855d..00000000 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Bootsharp.Publish; - -internal sealed class DeclarationGenerator (Preferences prefs) -{ - private readonly ModuleDeclarationGenerator modules = new(prefs); - private readonly TypeDeclarationGenerator types = new(prefs); - - public string Generate (SolutionInspection spec) => Fmt(0, - """import type { EventBroadcaster, EventSubscriber } from "../event.mjs";""", - types.Generate(spec), - modules.Generate(spec) - ) + "\n"; -} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs deleted file mode 100644 index 2ed14085..00000000 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Text; - -namespace Bootsharp.Publish; - -internal sealed class ModuleDeclarationGenerator (Preferences prefs) -{ - private readonly StringBuilder bld = new(); - private readonly TypeSyntaxBuilder ts = new(prefs); - - private MemberMeta member => members[index]; - private MemberMeta? prevMember => index == 0 ? null : members[index - 1]; - private MemberMeta? nextMember => index == members.Length - 1 ? null : members[index + 1]; - - private DocumentationBuilder docs = null!; - private MemberMeta[] members = null!; - private int index; - - public string Generate (SolutionInspection spec) - { - docs = new(spec.Documentation); - members = spec.Static - .Concat(spec.Modules.SelectMany(i => i.Members)) - .OrderBy(m => m.JSSpace).ToArray(); - for (index = 0; index < members.Length; index++) - DeclareMember(); - return bld.ToString(); - } - - private void DeclareMember () - { - if (ShouldOpenNamespace()) OpenNamespace(); - switch (member) - { - case EventMeta e: DeclareEvent(e); break; - case PropertyMeta { Interop: InteropKind.Export } p: DeclarePropertyExport(p); break; - case PropertyMeta { Interop: InteropKind.Import } p: DeclarePropertyImport(p); break; - case MethodMeta { Interop: InteropKind.Export } m: DeclareMethodExport(m); break; - case MethodMeta { Interop: InteropKind.Import } m: DeclareMethodImport(m); break; - } - if (ShouldCloseNamespace()) CloseNamespace(); - } - - private bool ShouldOpenNamespace () - { - if (prevMember is null) return true; - return prevMember.JSSpace != member.JSSpace; - } - - private void OpenNamespace () - { - bld.Append(docs.BuildType(member.Info.DeclaringType!, 0)); - bld.Append($"\nexport namespace {member.JSSpace} {{"); - } - - private bool ShouldCloseNamespace () - { - if (nextMember is null) return true; - return nextMember.JSSpace != member.JSSpace; - } - - private void CloseNamespace () - { - bld.Append("\n}"); - } - - private void DeclareEvent (EventMeta evt) - { - bld.Append(docs.BuildEvent(evt, 1)); - var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; - bld.Append($"\n export const {evt.JSName}: {type}<["); - bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); - bld.Append("]>;"); - } - - private void DeclarePropertyExport (PropertyMeta prop) - { - var mod = prop.CanGet && !prop.CanSet ? "const" : "let"; - var type = ts.BuildVariable(prop.Info); - bld.Append(docs.BuildProperty(prop.Info, 1)); - bld.Append($"\n export {mod} {prop.JSName}: {type};"); - } - - private void DeclarePropertyImport (PropertyMeta prop) - { - var type = ts.BuildVariable(prop.Info); - bld.Append(docs.BuildProperty(prop.Info, 1)); - bld.Append($"\n export let {prop.JSName}: {{ "); - if (prop.CanGet) bld.Append($"get: () => {type}"); - if (prop.CanGet && prop.CanSet) bld.Append("; "); - if (prop.CanSet) bld.Append($"set: (value: {type}) => void"); - bld.Append(" };"); - } - - private void DeclareMethodExport (MethodMeta method) - { - bld.Append(docs.BuildFunction(method, 1)); - bld.Append($"\n export function {method.JSName}("); - bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); - bld.Append($"): {ts.BuildReturn(method.Info)};"); - } - - private void DeclareMethodImport (MethodMeta method) - { - bld.Append(docs.BuildFunction(method, 1)); - bld.Append($"\n export let {method.JSName}: ("); - bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); - bld.Append($") => {ts.BuildReturn(method.Info)};"); - } -} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs deleted file mode 100644 index c84fbe88..00000000 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text; - -namespace Bootsharp.Publish; - -internal sealed class TypeDeclarationGenerator (Preferences prefs) -{ - private readonly StringBuilder bld = new(); - private readonly TypeSyntaxBuilder ts = new(prefs); - - private TypeMeta meta => metas[index]; - private TypeMeta? prevMeta => index == 0 ? null : metas[index - 1]; - private TypeMeta? nextMeta => index == metas.Count - 1 ? null : metas[index + 1]; - private int indent => !string.IsNullOrEmpty(GetNamespace(meta)) ? 1 : 0; - - private DocumentationBuilder docs = null!; - private readonly Dictionary metaByClr = []; - private readonly List metas = []; - private int index; - - public string Generate (SolutionInspection spec) - { - docs = new(spec.Documentation); - CollectMetas(spec); - for (index = 0; index < metas.Count; index++) - DeclareType(); - return bld.ToString(); - } - - private void CollectMetas (SolutionInspection spec) - { - foreach (var meta in spec.Instanced) - metas.Add(metaByClr[meta.Clr] = meta); - foreach (var meta in spec.Serialized) - if (metaByClr.TryAdd(meta.Clr, meta) && IsUserType(meta.Clr)) - metas.Add(meta); - metas.Sort((a, b) => string.Compare(GetNamespace(a), GetNamespace(b), StringComparison.Ordinal)); - } - - private void DeclareType () - { - if (ShouldOpenNamespace()) OpenNamespace(); - if (meta is InstancedMeta it) DeclareInstanced(it); - else if (meta is SerializedEnumMeta enu) DeclareEnum(enu); - else if (meta is SerializedObjectMeta obj) DeclareSerialized(obj); - if (ShouldCloseNamespace()) CloseNamespace(); - } - - private bool ShouldOpenNamespace () - { - if (string.IsNullOrEmpty(GetNamespace(meta))) return false; - if (prevMeta == null) return true; - return GetNamespace(prevMeta) != GetNamespace(meta); - } - - private void OpenNamespace () - { - var space = GetNamespace(meta); - AppendLine($"export namespace {space} {{", 0); - } - - private bool ShouldCloseNamespace () - { - if (string.IsNullOrEmpty(GetNamespace(meta))) return false; - if (nextMeta is null) return true; - return GetNamespace(nextMeta) != GetNamespace(meta); - } - - private void CloseNamespace () - { - AppendLine("}", 0); - } - - private void DeclareEnum (SerializedEnumMeta enu) - { - bld.Append(docs.BuildType(enu.Clr, indent)); - AppendLine($"export enum {enu.Clr.Name} {{", indent); - var names = Enum.GetNames(enu.Clr); - for (int i = 0; i < names.Length; i++) - { - bld.Append(docs.BuildProperty(enu.Clr.GetField(names[i])!, indent + 1)); - if (i == names.Length - 1) AppendLine(names[i], indent + 1); - else AppendLine($"{names[i]},", indent + 1); - } - AppendLine("}", indent); - } - - private void DeclareSerialized (SerializedObjectMeta obj) - { - bld.Append(docs.BuildType(obj.Clr, indent)); - AppendLine($"export type {ts.BuildName(obj.Clr)} = ", indent); - if (TryGetBase(obj.Clr, out var baseType)) - bld.Append(ts.BuildFullName(baseType)).Append(" & "); - bld.Append("Readonly<{"); - foreach (var prop in obj.Properties) - if (ShouldDeclareOn(obj.Clr, prop.Info)) - AppendProperty(prop); - AppendLine("}>;", indent); - - void AppendProperty (SerializedPropertyMeta prop) - { - bld.Append(docs.BuildProperty(prop.Info, indent + 1)); - AppendLine(prop.JSName, indent + 1); - bld.Append(ts.BuildProperty(prop.Info)); - bld.Append(';'); - } - } - - private void DeclareInstanced (InstancedMeta it) - { - bld.Append(docs.BuildType(it.Clr, indent)); - AppendLine($"export interface {ts.BuildName(it.Clr)}", indent); - AppendExtensions(); - bld.Append(" {"); - foreach (var member in it.Members) - if (!ShouldDeclareOn(it.Clr, member.Info)) continue; - else if (member is EventMeta evt) AppendEvent(evt); - else if (member is PropertyMeta prop) AppendProperty(prop); - else AppendMethod((MethodMeta)member); - AppendLine("}", indent); - - void AppendExtensions () - { - var ext = new List(it.Clr.GetInterfaces().Where(IsUserType)); - if (TryGetBase(it.Clr, out var baseType)) ext.Insert(0, baseType); - if (ext.Count > 0) bld.Append(" extends ").AppendJoin(", ", ext.Select(ts.BuildFullName)); - } - - void AppendEvent (EventMeta evt) - { - bld.Append(docs.BuildEvent(evt, indent + 1)); - AppendLine(evt.JSName, indent + 1); - var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; - bld.Append($": {type}<["); - bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); - bld.Append("]>;"); - } - - void AppendProperty (PropertyMeta prop) - { - bld.Append(docs.BuildProperty(prop.Info, indent + 1)); - var name = !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; - AppendLine(name, indent + 1); - bld.Append(ts.BuildProperty(prop.Info)); - bld.Append(';'); - } - - void AppendMethod (MethodMeta meta) - { - bld.Append(docs.BuildFunction(meta, indent + 1)); - AppendLine(meta.JSName, indent + 1); - bld.Append('('); - bld.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); - bld.Append("): "); - bld.Append(ts.BuildReturn(meta.Info)); - bld.Append(';'); - } - } - - private void AppendLine (string content, int level) - { - bld.Append('\n'); - Append(content, level); - } - - private void Append (string content, int level) - { - for (int i = 0; i < level * 4; i++) - bld.Append(' '); - bld.Append(content); - } - - private string GetNamespace (TypeMeta meta) - { - return BuildJSSpace(meta.Clr, prefs); - } - - private bool TryGetBase (Type clr, [NotNullWhen(true)] out Type? baseType) - { - if ((baseType = clr.BaseType) == null || !IsUserType(baseType)) return false; - return metaByClr.ContainsKey(baseType); - } - - private bool ShouldDeclareOn (Type host, MemberInfo member) - { - if (member.DeclaringType == host) return true; - return !TryGetBase(member.DeclaringType!, out _) && !TryGetBase(host, out _); - } -} diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index d189e3d7..8079ef84 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -41,30 +41,30 @@ + TaskName="Bootsharp.Publish.GenerateCS" AssemblyFile="$(BsPublishAssembly)"/> + TaskName="Bootsharp.Publish.GenerateJS" AssemblyFile="$(BsPublishAssembly)"/> - + - - + + @@ -83,7 +83,7 @@ - + $(PublishDir) @@ -112,14 +112,14 @@ - - + + @@ -133,6 +133,7 @@ + diff --git a/src/cs/Bootsharp/Build/PackageTemplate.json b/src/cs/Bootsharp/Build/PackageTemplate.json index be336b46..45cac5c2 100644 --- a/src/cs/Bootsharp/Build/PackageTemplate.json +++ b/src/cs/Bootsharp/Build/PackageTemplate.json @@ -2,7 +2,8 @@ "name": "%MODULE_NAME%", "type": "module", "exports": { - ".": "./%MODULE_DIR%/index.mjs" + ".": "./%MODULE_DIR%/index.mjs", + "./*": "./%MODULE_DIR%/generated/*.g.mjs" }, "browser": { "node:fs": false, diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 462527ef..48bc04db 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.227 + 0.8.0-alpha.297 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/package.json b/src/js/package.json index cf40f5d1..63c129c2 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -3,7 +3,7 @@ "build": "bash scripts/build.sh", "compile-test": "bash scripts/compile-test.sh", "test": "vitest run", - "cover": "vitest run --coverage --coverage.thresholds.100 --coverage.include=**/bootsharp/**/*.mjs" + "cover": "vitest run --coverage --coverage.thresholds.100 --coverage.include=**/bin/bootsharp/**/*.mjs" }, "devDependencies": { "typescript": "6.0.3", diff --git a/src/js/scripts/build.sh b/src/js/scripts/build.sh index 9a4c4ab2..ec52b9f8 100644 --- a/src/js/scripts/build.sh +++ b/src/js/scripts/build.sh @@ -2,4 +2,3 @@ rm -rf dist tsc --outDir dist --declaration mkdir -p dist/dotnet cp src/dotnet/*.d.ts dist/dotnet/ -rm dist/generated/*.g.mjs diff --git a/src/js/src/exports.mts b/src/js/src/exports.mts index 66368879..fec682d4 100644 --- a/src/js/src/exports.mts +++ b/src/js/src/exports.mts @@ -7,3 +7,18 @@ export async function bindExports(runtime: RuntimeAPI, assembly: string) { exports = asm["Bootsharp"]["Generated"]["Interop"] ?? {}; exports.disposeExported = asm["Bootsharp"]["Generated"]["Instances"].DisposeExported; } + +// noinspection JSUnusedGlobalSymbols (used by the generated code in debug mode) +export function getExport(name: string): (...args: unknown[]) => unknown { + return (...args) => { + if (exports == null) throw Error("Boot the runtime before invoking C# APIs."); + let result: unknown; + try { result = (exports[name] as (...args: unknown[]) => unknown)(...args); } + catch (error) { throw Error(`${(error as Error).message}\n${(error as Error).stack}`); } + if (typeof (result as { then?: unknown })?.then === "function") + return (result as Promise).catch(error => { + throw Error(`${(error as Error).message}\n${(error as Error).stack}`); + }); + return result; + }; +} diff --git a/src/js/src/generated/bindings.g.mts b/src/js/src/generated/bindings.g.mts deleted file mode 100644 index ff8b4c56..00000000 --- a/src/js/src/generated/bindings.g.mts +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/src/js/src/generated/imports.g.mts b/src/js/src/generated/imports.g.mts new file mode 100644 index 00000000..dae54630 --- /dev/null +++ b/src/js/src/generated/imports.g.mts @@ -0,0 +1,2 @@ +import type { RuntimeAPI } from "../dotnet/index.mjs"; +export function bindImports(runtime: RuntimeAPI): void {} diff --git a/src/js/src/generated/index.g.mts b/src/js/src/generated/index.g.mts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/src/js/src/generated/index.g.mts @@ -0,0 +1 @@ +export {}; diff --git a/src/js/src/imports.mts b/src/js/src/imports.mts index 2c6b37ea..42940239 100644 --- a/src/js/src/imports.mts +++ b/src/js/src/imports.mts @@ -1,10 +1,27 @@ -import * as generated from "./generated/bindings.g.mjs"; +import { Event } from "./event.mjs"; +import { bindImports as bindGeneratedImports } from "./generated/imports.g.mjs"; import { instances } from "./instances.mjs"; import type { RuntimeAPI } from "./dotnet/index.mjs"; export function bindImports(runtime: RuntimeAPI) { - runtime.setModuleImports("Bootsharp", { - ...generated, - instances - }); + bindGeneratedImports(runtime); + runtime.setModuleImports("Bootsharp", { instances }); +} + +// noinspection JSUnusedGlobalSymbols (used by the generated code) +export function importEvent(handler: (...args: [...T]) => void): Event { + const event = new Event(); + const broadcast = event.broadcast.bind(event); + event.broadcast = (...args: [...T]) => { + broadcast(...args); + handler(...args); + }; + return event; +} + +// noinspection JSUnusedGlobalSymbols (used by the generated code in debug mode) +export function getImport(handler: unknown, serializedHandler: T, name: string): T { + if (typeof handler !== "function") + throw Error(`Failed to invoke '${name}' from C#. Make sure to assign the function in JavaScript.`); + return serializedHandler; } diff --git a/src/js/src/index.mts b/src/js/src/index.mts index de8c246b..2069c1b1 100644 --- a/src/js/src/index.mts +++ b/src/js/src/index.mts @@ -12,6 +12,6 @@ export default { }; export * from "./event.mjs"; -export * from "./generated/bindings.g.mjs"; +export * from "./generated/index.g.mjs"; export type { BootOptions } from "./boot.mjs"; export type { BootManifest, BootResources, BinaryResource } from "./resources.mjs"; diff --git a/src/js/src/instances.mts b/src/js/src/instances.mts index 6c7fbe78..a7894eba 100644 --- a/src/js/src/instances.mts +++ b/src/js/src/instances.mts @@ -6,28 +6,31 @@ const importedById = new Map(); const idByImported = new Map(); const onDisposeById = new Map void>(); const idPool = new Array(); -let nextId = -2147483648; // Number.MIN_SAFE_INTEGER is below C#'s Int32.MinValue +let nextId = 0; // JS IDs are always positive; C#'s — negative. export const instances = { - /** Invokes the specified factory to create and register an exported instance wrapper associated with the ID, - * unless an exported instance is already registered under the ID, in which case returns its wrapper. */ - export(id: number, factory: (id: number) => T): T { - const instance = exportedById.get(id)?.deref() as T | undefined; - if (instance != null) return instance; - const exported = factory(id); - exportedById.set(id, new WeakRef(exported)); - exportedFinalizer.register(exported, id); - return exported; + /** 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; + const exported = exportedById.get(id)?.deref() as T; + if (exported != null) return exported; + const proxy = new factory(id); + exportedById.set(id, new WeakRef(proxy)); + exportedFinalizer.register(proxy, id); + return proxy; }, - /** Registers specified imported instance and associates it with a unique ID, unless it's already registered, - * in which case the ID of the registered instance is returned. */ - import(instance: object, factory?: (id: number) => () => void): number { - const registered = idByImported.get(instance); - if (registered !== undefined) return registered; + /** 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 { + const exportedId = (instance as { _id: number })?._id; + if (exportedId !== undefined) return exportedId; + const importedId = idByImported.get(instance); + if (importedId !== undefined) return importedId; const id = idPool.length > 0 ? idPool.pop()! : nextId++; importedById.set(id, instance); idByImported.set(instance, id); - if (factory != null) onDisposeById.set(id, factory(id)); + if (cb != null) onDisposeById.set(id, cb(id)); return id; }, /** Returns a registered imported instance associated with the specified ID. */ diff --git a/src/js/src/serialization.mts b/src/js/src/serialization.mts deleted file mode 100644 index 9df383eb..00000000 --- a/src/js/src/serialization.mts +++ /dev/null @@ -1,442 +0,0 @@ -import { malloc, free, getHeap } from "./runtime.mjs"; - -export type Binary = { - write: Write; - read: Read; - arrayCtor?: TypedArrayCtor; -}; - -type Write = (writer: Writer, value: T) => void; -type Read = (reader: Reader) => T | null; -type TypedArray = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigInt64Array | Float32Array | Float64Array; -type TypedArrayCtor = new(length: number) => TypedArray; - -const utf16Decoder = new TextDecoder("utf-16le"); -const dotnetEpochTicks = 621355968000000000n; - -export function serialize(value: T | null | undefined, type: Binary): bigint { - if (value == null) return 0n; - const writer = new Writer(); - type.write(writer, value); - return writer.detach(); -} - -export function deserialize(handle: bigint | null | undefined, type: Binary): T | null { - if (handle == null || handle === 0n) return null; - const reader = new Reader(handle); - const result = type.read(reader); - reader.dispose(); - return result; -} - -export function binary(write: Write, read: Read, arrayCtor?: TypedArrayCtor): Binary { - return { write, read, arrayCtor }; -} - -export const types = { - Boolean: binary( - (writer, value: boolean) => writer.writeBool(value), - reader => reader.readBool()), - - Byte: binary( - (writer, value: number) => writer.writeByte(value), - reader => reader.readByte(), Uint8Array), - - SByte: binary( - (writer, value: number) => writer.writeSByte(value), - reader => reader.readSByte(), Int8Array), - - Int16: binary( - (writer, value: number) => writer.writeInt16(value), - reader => reader.readInt16(), Int16Array), - - UInt16: binary( - (writer, value: number) => writer.writeUInt16(value), - reader => reader.readUInt16(), Uint16Array), - - Int32: binary( - (writer, value: number) => writer.writeInt32(value), - reader => reader.readInt32(), Int32Array), - - UInt32: binary( - (writer, value: number) => writer.writeUInt32(value), - reader => reader.readUInt32(), Uint32Array), - - Int64: binary( - (writer, value: bigint) => writer.writeInt64(value), - reader => reader.readInt64(), BigInt64Array), - - UInt64: binary( - (writer, value: number) => writer.writeUInt64(value), - reader => Number(reader.readUInt64())), - - IntPtr: binary( - (writer, value: number) => writer.writeInt64(BigInt(value)), - reader => Number(reader.readInt64())), - - Single: binary( - (writer, value: number) => writer.writeSingle(value), - reader => reader.readSingle(), Float32Array), - - Double: binary( - (writer, value: number) => writer.writeDouble(value), - reader => reader.readDouble(), Float64Array), - - Decimal: binary( - (writer, value: number) => writer.writeDouble(value), - reader => reader.readDouble()), - - Char: binary( - (writer, value: string) => writer.writeUInt16(((String(value ?? ""))[0] ?? "\0").charCodeAt(0)), - reader => String.fromCharCode(reader.readUInt16())), - - String: binary( - (writer, value: string | null | undefined) => writer.writeString(value), - reader => reader.readString()), - - DateTime: binary( - (writer, value: Date) => writer.writeInt64((BigInt(value.getTime()) * 10000n) + dotnetEpochTicks), - reader => new Date(Number((reader.readInt64() - dotnetEpochTicks) / 10000n))), - - DateTimeOffset: binary( - (writer, value: Date) => writer.writeInt64((BigInt(value.getTime()) * 10000n) + dotnetEpochTicks), - reader => new Date(Number((reader.readInt64() - dotnetEpochTicks) / 10000n))), - - Nullable: (inner: Binary): Binary => binary( - (writer, value) => writeNullable(writer, value, inner), - reader => readNullable(reader, inner)), - - Array: (element: Binary): Binary | null | undefined> => binary( - (writer, value) => writeArray(writer, value, element), - reader => readArray(reader, element)), - - List: (element: Binary): Binary | null | undefined> => binary( - (writer, value) => writeList(writer, value, element), - reader => readList(reader, element)), - - Dictionary: (key: Binary, value: Binary): Binary | null | undefined> => binary( - (writer, map) => writeDictionary(writer, map, key, value), - reader => readDictionary(reader, key, value)) -}; - -function writeNullable(writer: Writer, value: T | null | undefined, inner: Binary): void { - writer.writeBool(value != null); - if (value != null) inner.write(writer, value); -} - -function readNullable(reader: Reader, inner: Binary): T | null { - return reader.readBool() ? inner.read(reader) : null; -} - -function writeArray(writer: Writer, value: ArrayLike | null | undefined, element: Binary): void { - if (value == null) { - writer.writeMeta(-1); - return; - } - writer.writeMeta(value.length); - if (element.arrayCtor && value instanceof element.arrayCtor) - writer.writeBytes(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)); - else for (let i = 0; i < value.length; i++) - element.write(writer, value[i]!); -} - -function readArray(reader: Reader, element: Binary): ArrayLike | null { - const count = reader.readMeta(); - if (count < 0) return null; - if (element.arrayCtor) { - const result = new element.arrayCtor(count); - reader.readBytes(new Uint8Array(result.buffer, result.byteOffset, result.byteLength)); - return result as unknown as ArrayLike; - } - const result = new Array(count); - for (let i = 0; i < count; i++) - result[i] = element.read(reader)!; - return result; -} - -function writeList(writer: Writer, value: ArrayLike | null | undefined, element: Binary): void { - if (value == null) { - writer.writeMeta(-1); - return; - } - writer.writeMeta(value.length); - for (let i = 0; i < value.length; i++) - element.write(writer, value[i]!); -} - -function readList(reader: Reader, element: Binary): T[] | null { - const count = reader.readMeta(); - if (count < 0) return null; - const result = new Array(count); - for (let i = 0; i < count; i++) - result[i] = element.read(reader)!; - return result; -} - -function writeDictionary(writer: Writer, map: Map | null | undefined, key: Binary, value: Binary): void { - if (map == null) { - writer.writeMeta(-1); - return; - } - writer.writeMeta(map.size); - for (const pair of map) { - key.write(writer, pair[0]); - value.write(writer, pair[1]); - } -} - -function readDictionary(reader: Reader, key: Binary, value: Binary): Map | null { - const count = reader.readMeta(); - if (count < 0) return null; - const result = new Map(); - for (let i = 0; i < count; i++) - result.set(key.read(reader)!, value.read(reader)!); - return result; -} - -class Writer { - private heap: Uint8Array; - private ptr: number; - private offset: number; - private capacity: number; - private view: DataView; - - constructor() { - this.capacity = 256; - this.ptr = malloc(this.capacity); - this.offset = 0; - this.heap = getHeap(); - this.view = new DataView(this.heap.buffer, this.heap.byteOffset); - } - - detach(): bigint { - const handle = BigInt(this.ptr >>> 0); - this.ptr = 0; - this.capacity = 0; - this.offset = 0; - return handle; - } - - writeMeta(value: number): void { - let zigzag = ((value << 1) ^ (value >> 31)) >>> 0; - this.ensure(5); - let position = this.ptr + this.offset; - while (zigzag >= 0x80) { - this.heap[position++] = (zigzag | 0x80) & 0xff; - zigzag >>>= 7; - } - this.heap[position++] = zigzag; - this.offset = position - this.ptr; - } - - writeString(value: string | null | undefined): void { - if (value == null) { - this.writeMeta(-1); - return; - } - const length = value.length; - const bytes = length * 2; - this.writeMeta(length); - this.ensure(bytes); - const base = this.ptr + this.offset; - for (let i = 0, p = base; i < length; i++, p += 2) - this.view.setUint16(p, value.charCodeAt(i), true); - this.offset += bytes; - } - - writeBytes(value: Uint8Array): void { - this.ensure(value.byteLength); - this.heap.set(value, this.ptr + this.offset); - this.offset += value.byteLength; - } - - writeByte(value: number): void { - this.ensure(1); - this.heap[this.ptr + this.offset++] = value & 0xff; - } - - writeSByte(value: number): void { - this.writeByte(value); - } - - writeBool(value: boolean): void { - this.writeByte(value ? 1 : 0); - } - - writeUInt16(value: number): void { - this.ensure(2); - this.view.setUint16(this.ptr + this.offset, value, true); - this.offset += 2; - } - - writeInt16(value: number): void { - this.ensure(2); - this.view.setInt16(this.ptr + this.offset, value, true); - this.offset += 2; - } - - writeUInt32(value: number): void { - this.ensure(4); - this.view.setUint32(this.ptr + this.offset, value, true); - this.offset += 4; - } - - writeInt32(value: number): void { - this.ensure(4); - this.view.setInt32(this.ptr + this.offset, value, true); - this.offset += 4; - } - - writeUInt64(value: bigint | number): void { - this.ensure(8); - this.view.setBigUint64(this.ptr + this.offset, BigInt(value), true); - this.offset += 8; - } - - writeInt64(value: bigint | number): void { - this.ensure(8); - this.view.setBigInt64(this.ptr + this.offset, BigInt(value), true); - this.offset += 8; - } - - writeSingle(value: number): void { - this.ensure(4); - this.view.setFloat32(this.ptr + this.offset, value, true); - this.offset += 4; - } - - writeDouble(value: number): void { - this.ensure(8); - this.view.setFloat64(this.ptr + this.offset, value, true); - this.offset += 8; - } - - private ensure(count: number): void { - if (this.capacity - this.offset >= count) return; - const capacity = Math.max(this.capacity * 2, this.offset + count); - const sourcePtr = this.ptr; - const ptr = malloc(capacity); - this.refreshHeapView(); - this.heap.copyWithin(ptr, sourcePtr, sourcePtr + this.offset); - free(sourcePtr); - this.ptr = ptr; - this.capacity = capacity; - } - - private refreshHeapView(): void { - const heap = getHeap(); - /* v8 ignore start -- @preserve */ // Uncoverable, as WASM heap growth is not controllable. - if (this.heap === heap) return; - /* v8 ignore stop -- @preserve */ - this.heap = heap; - this.view = new DataView(heap.buffer, heap.byteOffset); - } -} - -class Reader { - private readonly heap: Uint8Array; - private readonly ptr: number; - private offset: number; - private view: DataView; - - constructor(handle: bigint) { - this.ptr = Number(handle & 0xffffffffn); - this.offset = 0; - this.heap = getHeap(); - this.view = new DataView(this.heap.buffer, this.heap.byteOffset); - } - - dispose(): void { - free(this.ptr); - } - - readMeta(): number { - let result = 0; - let shift = 0; - let next; - let position = this.ptr + this.offset; - do { - next = this.heap[position++]; - result = (result | ((next & 0x7f) << shift)) >>> 0; - shift += 7; - } while ((next & 0x80) !== 0); - this.offset = position - this.ptr; - return (result >>> 1) ^ -(result & 1); - } - - readString(): string | null { - const count = this.readMeta(); - if (count < 0) return null; - const bytes = count * 2; - const start = this.ptr + this.offset; - const value = utf16Decoder.decode(this.heap.subarray(start, start + bytes)); - this.offset += bytes; - return value; - } - - readBytes(destination: Uint8Array): void { - destination.set(this.heap.subarray(this.ptr + this.offset, this.ptr + this.offset + destination.byteLength)); - this.offset += destination.byteLength; - } - - readByte(): number { - return this.heap[this.ptr + this.offset++]; - } - - readSByte(): number { - const value = this.readByte(); - return value > 127 ? value - 256 : value; - } - - readBool(): boolean { - return this.readByte() !== 0; - } - - readUInt16(): number { - const value = this.view.getUint16(this.ptr + this.offset, true); - this.offset += 2; - return value; - } - - readInt16(): number { - const value = this.view.getInt16(this.ptr + this.offset, true); - this.offset += 2; - return value; - } - - readUInt32(): number { - const value = this.view.getUint32(this.ptr + this.offset, true); - this.offset += 4; - return value; - } - - readInt32(): number { - const value = this.view.getInt32(this.ptr + this.offset, true); - this.offset += 4; - return value; - } - - readUInt64(): bigint { - const value = this.view.getBigUint64(this.ptr + this.offset, true); - this.offset += 8; - return value; - } - - readInt64(): bigint { - const value = this.view.getBigInt64(this.ptr + this.offset, true); - this.offset += 8; - return value; - } - - readSingle(): number { - const value = this.view.getFloat32(this.ptr + this.offset, true); - this.offset += 4; - return value; - } - - readDouble(): number { - const value = this.view.getFloat64(this.ptr + this.offset, true); - this.offset += 8; - return value; - } -} diff --git a/src/js/src/serialization/index.mts b/src/js/src/serialization/index.mts new file mode 100644 index 00000000..d7602d27 --- /dev/null +++ b/src/js/src/serialization/index.mts @@ -0,0 +1,12 @@ +import { serialize, deserialize, binary } from "./serializer.mjs"; +import { std } from "./std.mjs"; + +const serialization: { + serialize: typeof serialize; + deserialize: typeof deserialize; + binary: typeof binary; + std: typeof std; + [factoryId: string]: unknown; +} = { serialize, deserialize, binary, std }; + +export default serialization; diff --git a/src/js/src/serialization/reader.mts b/src/js/src/serialization/reader.mts new file mode 100644 index 00000000..d92b66d5 --- /dev/null +++ b/src/js/src/serialization/reader.mts @@ -0,0 +1,111 @@ +import { getHeap, free } from "../runtime.mjs"; + +const utf16Decoder = new TextDecoder("utf-16le"); + +export class Reader { + private readonly heap: Uint8Array; + private readonly ptr: number; + private offset: number; + private view: DataView; + + constructor(handle: bigint) { + this.ptr = Number(handle & 0xffffffffn); + this.offset = 0; + this.heap = getHeap(); + this.view = new DataView(this.heap.buffer, this.heap.byteOffset); + } + + dispose(): void { + free(this.ptr); + } + + readMeta(): number { + let result = 0; + let shift = 0; + let next; + let position = this.ptr + this.offset; + do { + next = this.heap[position++]; + result = (result | ((next & 0x7f) << shift)) >>> 0; + shift += 7; + } while ((next & 0x80) !== 0); + this.offset = position - this.ptr; + return (result >>> 1) ^ -(result & 1); + } + + readString(): string | null { + const count = this.readMeta(); + if (count < 0) return null; + const bytes = count * 2; + const start = this.ptr + this.offset; + const value = utf16Decoder.decode(this.heap.subarray(start, start + bytes)); + this.offset += bytes; + return value; + } + + readBytes(destination: Uint8Array): void { + destination.set(this.heap.subarray(this.ptr + this.offset, this.ptr + this.offset + destination.byteLength)); + this.offset += destination.byteLength; + } + + readByte(): number { + return this.heap[this.ptr + this.offset++]; + } + + readSByte(): number { + const value = this.readByte(); + return value > 127 ? value - 256 : value; + } + + readBool(): boolean { + return this.readByte() !== 0; + } + + readUInt16(): number { + const value = this.view.getUint16(this.ptr + this.offset, true); + this.offset += 2; + return value; + } + + readInt16(): number { + const value = this.view.getInt16(this.ptr + this.offset, true); + this.offset += 2; + return value; + } + + readUInt32(): number { + const value = this.view.getUint32(this.ptr + this.offset, true); + this.offset += 4; + return value; + } + + readInt32(): number { + const value = this.view.getInt32(this.ptr + this.offset, true); + this.offset += 4; + return value; + } + + readUInt64(): bigint { + const value = this.view.getBigUint64(this.ptr + this.offset, true); + this.offset += 8; + return value; + } + + readInt64(): bigint { + const value = this.view.getBigInt64(this.ptr + this.offset, true); + this.offset += 8; + return value; + } + + readSingle(): number { + const value = this.view.getFloat32(this.ptr + this.offset, true); + this.offset += 4; + return value; + } + + readDouble(): number { + const value = this.view.getFloat64(this.ptr + this.offset, true); + this.offset += 8; + return value; + } +} diff --git a/src/js/src/serialization/serializer.mts b/src/js/src/serialization/serializer.mts new file mode 100644 index 00000000..b6d14c41 --- /dev/null +++ b/src/js/src/serialization/serializer.mts @@ -0,0 +1,32 @@ +import { Reader } from "./reader.mjs"; +import { Writer } from "./writer.mjs"; + +export type Binary = { + write: Write; + read: Read; + arrayCtor?: TypedArrayCtor; +}; + +type Write = (writer: Writer, value: T) => void; +type Read = (reader: Reader) => T | null; +type TypedArrayCtor = new(length: number) => TypedArray; +type TypedArray = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigInt64Array | Float32Array | Float64Array; + +export function binary(write: Write, read: Read, arrayCtor?: TypedArrayCtor): Binary { + return { write, read, arrayCtor }; +} + +export function serialize(value: T | null | undefined, type: Binary): bigint { + if (value == null) return 0n; + const writer = new Writer(); + type.write(writer, value); + return writer.detach(); +} + +export function deserialize(handle: bigint | null | undefined, type: Binary): T | null { + if (handle == null || handle === 0n) return null; + const reader = new Reader(handle); + const result = type.read(reader); + reader.dispose(); + return result; +} diff --git a/src/js/src/serialization/std.mts b/src/js/src/serialization/std.mts new file mode 100644 index 00000000..047ba6be --- /dev/null +++ b/src/js/src/serialization/std.mts @@ -0,0 +1,166 @@ +import { Binary, binary } from "./serializer.mjs"; +import { Reader } from "./reader.mjs"; +import { Writer } from "./writer.mjs"; + +const dotnetEpochTicks = 621355968000000000n; + +export const std = { + Boolean: binary( + (writer, value: boolean) => writer.writeBool(value), + reader => reader.readBool()), + + Byte: binary( + (writer, value: number) => writer.writeByte(value), + reader => reader.readByte(), Uint8Array), + + SByte: binary( + (writer, value: number) => writer.writeSByte(value), + reader => reader.readSByte(), Int8Array), + + Int16: binary( + (writer, value: number) => writer.writeInt16(value), + reader => reader.readInt16(), Int16Array), + + UInt16: binary( + (writer, value: number) => writer.writeUInt16(value), + reader => reader.readUInt16(), Uint16Array), + + Int32: binary( + (writer, value: number) => writer.writeInt32(value), + reader => reader.readInt32(), Int32Array), + + UInt32: binary( + (writer, value: number) => writer.writeUInt32(value), + reader => reader.readUInt32(), Uint32Array), + + Int64: binary( + (writer, value: bigint) => writer.writeInt64(value), + reader => reader.readInt64(), BigInt64Array), + + UInt64: binary( + (writer, value: number) => writer.writeUInt64(value), + reader => Number(reader.readUInt64())), + + IntPtr: binary( + (writer, value: number) => writer.writeInt64(BigInt(value)), + reader => Number(reader.readInt64())), + + Single: binary( + (writer, value: number) => writer.writeSingle(value), + reader => reader.readSingle(), Float32Array), + + Double: binary( + (writer, value: number) => writer.writeDouble(value), + reader => reader.readDouble(), Float64Array), + + Decimal: binary( + (writer, value: number) => writer.writeDouble(value), + reader => reader.readDouble()), + + Char: binary( + (writer, value: string) => writer.writeUInt16(((String(value ?? ""))[0] ?? "\0").charCodeAt(0)), + reader => String.fromCharCode(reader.readUInt16())), + + String: binary( + (writer, value: string | null | undefined) => writer.writeString(value), + reader => reader.readString()), + + DateTime: binary( + (writer, value: Date) => writer.writeInt64((BigInt(value.getTime()) * 10000n) + dotnetEpochTicks), + reader => new Date(Number((reader.readInt64() - dotnetEpochTicks) / 10000n))), + + DateTimeOffset: binary( + (writer, value: Date) => writer.writeInt64((BigInt(value.getTime()) * 10000n) + dotnetEpochTicks), + reader => new Date(Number((reader.readInt64() - dotnetEpochTicks) / 10000n))), + + Nullable: (inner: Binary): Binary => binary( + (writer, value) => writeNullable(writer, value, inner), + reader => readNullable(reader, inner)), + + Array: (element: Binary): Binary | null | undefined> => binary( + (writer, value) => writeArray(writer, value, element), + reader => readArray(reader, element)), + + List: (element: Binary): Binary | null | undefined> => binary( + (writer, value) => writeList(writer, value, element), + reader => readList(reader, element)), + + Dictionary: (key: Binary, value: Binary): Binary | null | undefined> => binary( + (writer, map) => writeDictionary(writer, map, key, value), + reader => readDictionary(reader, key, value)) +}; + +function writeNullable(writer: Writer, value: T | null | undefined, inner: Binary): void { + writer.writeBool(value != null); + if (value != null) inner.write(writer, value); +} + +function readNullable(reader: Reader, inner: Binary): T | null { + return reader.readBool() ? inner.read(reader) : null; +} + +function writeArray(writer: Writer, value: ArrayLike | null | undefined, element: Binary): void { + if (value == null) { + writer.writeMeta(-1); + return; + } + writer.writeMeta(value.length); + if (element.arrayCtor && value instanceof element.arrayCtor) + writer.writeBytes(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)); + else for (let i = 0; i < value.length; i++) + element.write(writer, value[i]!); +} + +function readArray(reader: Reader, element: Binary): ArrayLike | null { + const count = reader.readMeta(); + if (count < 0) return null; + if (element.arrayCtor) { + const result = new element.arrayCtor(count); + reader.readBytes(new Uint8Array(result.buffer, result.byteOffset, result.byteLength)); + return result as unknown as ArrayLike; + } + const result = new Array(count); + for (let i = 0; i < count; i++) + result[i] = element.read(reader)!; + return result; +} + +function writeList(writer: Writer, value: ArrayLike | null | undefined, element: Binary): void { + if (value == null) { + writer.writeMeta(-1); + return; + } + writer.writeMeta(value.length); + for (let i = 0; i < value.length; i++) + element.write(writer, value[i]!); +} + +function readList(reader: Reader, element: Binary): T[] | null { + const count = reader.readMeta(); + if (count < 0) return null; + const result = new Array(count); + for (let i = 0; i < count; i++) + result[i] = element.read(reader)!; + return result; +} + +function writeDictionary(writer: Writer, map: Map | null | undefined, key: Binary, value: Binary): void { + if (map == null) { + writer.writeMeta(-1); + return; + } + writer.writeMeta(map.size); + for (const pair of map) { + key.write(writer, pair[0]); + value.write(writer, pair[1]); + } +} + +function readDictionary(reader: Reader, key: Binary, value: Binary): Map | null { + const count = reader.readMeta(); + if (count < 0) return null; + const result = new Map(); + for (let i = 0; i < count; i++) + result.set(key.read(reader)!, value.read(reader)!); + return result; +} diff --git a/src/js/src/serialization/writer.mts b/src/js/src/serialization/writer.mts new file mode 100644 index 00000000..95bdd1ee --- /dev/null +++ b/src/js/src/serialization/writer.mts @@ -0,0 +1,140 @@ +import { getHeap, malloc, free } from "../runtime.mjs"; + +export class Writer { + private heap: Uint8Array; + private ptr: number; + private offset: number; + private capacity: number; + private view: DataView; + + constructor() { + this.capacity = 256; + this.ptr = malloc(this.capacity); + this.offset = 0; + this.heap = getHeap(); + this.view = new DataView(this.heap.buffer, this.heap.byteOffset); + } + + detach(): bigint { + const handle = BigInt(this.ptr >>> 0); + this.ptr = 0; + this.capacity = 0; + this.offset = 0; + return handle; + } + + writeMeta(value: number): void { + let zigzag = ((value << 1) ^ (value >> 31)) >>> 0; + this.ensure(5); + let position = this.ptr + this.offset; + while (zigzag >= 0x80) { + this.heap[position++] = (zigzag | 0x80) & 0xff; + zigzag >>>= 7; + } + this.heap[position++] = zigzag; + this.offset = position - this.ptr; + } + + writeString(value: string | null | undefined): void { + if (value == null) { + this.writeMeta(-1); + return; + } + const length = value.length; + const bytes = length * 2; + this.writeMeta(length); + this.ensure(bytes); + const base = this.ptr + this.offset; + for (let i = 0, p = base; i < length; i++, p += 2) + this.view.setUint16(p, value.charCodeAt(i), true); + this.offset += bytes; + } + + writeBytes(value: Uint8Array): void { + this.ensure(value.byteLength); + this.heap.set(value, this.ptr + this.offset); + this.offset += value.byteLength; + } + + writeByte(value: number): void { + this.ensure(1); + this.heap[this.ptr + this.offset++] = value & 0xff; + } + + writeSByte(value: number): void { + this.writeByte(value); + } + + writeBool(value: boolean): void { + this.writeByte(value ? 1 : 0); + } + + writeUInt16(value: number): void { + this.ensure(2); + this.view.setUint16(this.ptr + this.offset, value, true); + this.offset += 2; + } + + writeInt16(value: number): void { + this.ensure(2); + this.view.setInt16(this.ptr + this.offset, value, true); + this.offset += 2; + } + + writeUInt32(value: number): void { + this.ensure(4); + this.view.setUint32(this.ptr + this.offset, value, true); + this.offset += 4; + } + + writeInt32(value: number): void { + this.ensure(4); + this.view.setInt32(this.ptr + this.offset, value, true); + this.offset += 4; + } + + writeUInt64(value: bigint | number): void { + this.ensure(8); + this.view.setBigUint64(this.ptr + this.offset, BigInt(value), true); + this.offset += 8; + } + + writeInt64(value: bigint | number): void { + this.ensure(8); + this.view.setBigInt64(this.ptr + this.offset, BigInt(value), true); + this.offset += 8; + } + + writeSingle(value: number): void { + this.ensure(4); + this.view.setFloat32(this.ptr + this.offset, value, true); + this.offset += 4; + } + + writeDouble(value: number): void { + this.ensure(8); + this.view.setFloat64(this.ptr + this.offset, value, true); + this.offset += 8; + } + + private ensure(count: number): void { + if (this.capacity - this.offset >= count) return; + const capacity = Math.max(this.capacity * 2, this.offset + count); + const sourcePtr = this.ptr; + const ptr = malloc(capacity); + this.refreshHeapView(); + this.heap.copyWithin(ptr, sourcePtr, sourcePtr + this.offset); + free(sourcePtr); + this.ptr = ptr; + this.capacity = capacity; + } + + private refreshHeapView(): void { + const heap = getHeap(); + /* v8 ignore start -- @preserve */ // Uncoverable, as WASM heap growth is not controllable. + if (this.heap === heap) return; + /* v8 ignore stop -- @preserve */ + this.heap = heap; + this.view = new DataView(heap.buffer, heap.byteOffset); + } +} diff --git a/src/js/test/cs.ts b/src/js/test/cs.ts index 87940193..28983b5c 100644 --- a/src/js/test/cs.ts +++ b/src/js/test/cs.ts @@ -1,22 +1,33 @@ import assert from "node:assert"; import { resolve } from "node:path"; -import { readFileSync, existsSync } from "node:fs"; -import bootsharp, { Test, BootResources, BinaryResource } from "./cs/Test/bin/bootsharp/index.mjs"; +import { readFileSync, readdirSync, statSync, existsSync } from "node:fs"; +import bootsharp, { BootResources, BinaryResource } from "./cs/Test/bin/bootsharp/index.mjs"; +import { Program } from "./cs/Test/bin/bootsharp/generated/test.g.mjs"; -export { bootsharp, Test }; +export { bootsharp }; export * from "./cs/Test/bin/bootsharp/index.mjs"; export const resources = loadResources(); export const manifest = bootsharp.manifest; export async function bootRuntime() { - Test.Program.onMainInvoked = () => {}; + Program.onMainInvoked = () => {}; await bootsharp.boot(resources); } export function getDeclarations() { - const file = resolvePath("test/cs/Test/bin/bootsharp/generated/bindings.g.d.mts"); - return readFileSync(file).toString(); + const dir = resolvePath("test/cs/Test/bin/bootsharp/generated"); + return readAllDeclarations(dir); +} + +function readAllDeclarations(dir: string): string { + let out = ""; + for (const entry of readdirSync(dir)) { + const full = `${dir}/${entry}`; + if (statSync(full).isDirectory()) out += readAllDeclarations(full); + else if (entry.endsWith(".g.d.mts")) out += readFileSync(full).toString(); + } + return out; } function loadResources(): BootResources { diff --git a/src/js/test/cs/Test.Library/Modules/Bidirectional.cs b/src/js/test/cs/Test.Library/Modules/Bidirectional.cs new file mode 100644 index 00000000..7d2153db --- /dev/null +++ b/src/js/test/cs/Test.Library/Modules/Bidirectional.cs @@ -0,0 +1,12 @@ +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/IBidirectional.cs b/src/js/test/cs/Test.Library/Modules/IBidirectional.cs new file mode 100644 index 00000000..0c0277fa --- /dev/null +++ b/src/js/test/cs/Test.Library/Modules/IBidirectional.cs @@ -0,0 +1,12 @@ +using System; + +namespace Test.Library; + +public interface IBidirectional +{ + event Action? OnBiChanged; + + IBidirectional Bi { get; set; } + + IBidirectional EchoBi (IBidirectional bi); +} diff --git a/src/js/test/cs/Test.Library/Modules/Modules.cs b/src/js/test/cs/Test.Library/Modules/Modules.cs index 2bb59bd9..5681723f 100644 --- a/src/js/test/cs/Test.Library/Modules/Modules.cs +++ b/src/js/test/cs/Test.Library/Modules/Modules.cs @@ -4,7 +4,7 @@ namespace Test.Library; -public static class Modules +public static partial class Modules { [Export] public static async Task CanInteropWithImportedModuleAsync () @@ -55,6 +55,28 @@ public static void CanInteropWithImportedInnerInstance (IImportedInstanced impor inner.OnCountChanged -= handler; } + [Export] public static IBidirectional ExportBi () => new Bidirectional(); + [Import] public static partial IBidirectional ImportBi (); + + [Export] + public static void CanInteropWithBidirectional () + { + var js = ImportBi(); + var cs = new Bidirectional(); + IBidirectional? observed = null; + Action handler = b => observed = b; + js.OnBiChanged += handler; + Assert(js.EchoBi(js) == js); + Assert(js.EchoBi(cs) == cs); + js.Bi = cs; + Assert(observed == cs); + Assert(js.Bi == cs); + js.Bi = js; + Assert(observed == js); + Assert(js.Bi == js); + js.OnBiChanged -= handler; + } + [Export] public static async Task GetImportedArgsAndFinalize (string arg1, string arg2) { diff --git a/src/js/test/cs/Test.Library/Registries/Registries.cs b/src/js/test/cs/Test.Library/Registries/Registries.cs index 800ce499..ecd09614 100644 --- a/src/js/test/cs/Test.Library/Registries/Registries.cs +++ b/src/js/test/cs/Test.Library/Registries/Registries.cs @@ -20,6 +20,7 @@ public static IRegistry EchoRegistry (IRegistry registry) return registry; } + [Export] public static IRegistry MakeRegistry () => new Registry(); [Export] public static Vehicle?[]? EchoVehicles (Vehicle?[]? value) => value; [Export] public static Record?[]? EchoRecords (Record?[]? value) => value; diff --git a/src/js/test/cs/Test.Library/Registries/Registry.cs b/src/js/test/cs/Test.Library/Registries/Registry.cs new file mode 100644 index 00000000..1e42ebaa --- /dev/null +++ b/src/js/test/cs/Test.Library/Registries/Registry.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Test.Library; + +public class Registry : IRegistry +{ + public IReadOnlyList Wheeled { get; set; } = []; + public IReadOnlyList Tracked { get; set; } = []; +} diff --git a/src/js/test/cs/Test/package.json b/src/js/test/cs/Test/package.json index 2c5ec2ab..92794570 100644 --- a/src/js/test/cs/Test/package.json +++ b/src/js/test/cs/Test/package.json @@ -2,7 +2,8 @@ "name": "bootsharp", "type": "module", "exports": { - ".": "./bin/bootsharp/index.mjs" + ".": "./bin/bootsharp/index.mjs", + "./*": "./bin/bootsharp/generated/*.g.mjs" }, "browser": { "node:fs": false, diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index af27c680..23ccb6e9 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -6,8 +6,9 @@ import type { BootOptions } from "../cs/Test/bin/bootsharp/index.mjs"; async function setup() { vi.resetModules(); const cs = await import("../cs"); - cs.Test.Program.onMainInvoked = vi.fn(); - return cs; + const test = await import("../cs/Test/bin/bootsharp/generated/test.g.mjs"); + test.Program.onMainInvoked = vi.fn(); + return { ...cs, Program: test.Program }; } describe("boot", () => { @@ -38,9 +39,9 @@ describe("boot", () => { await boot; }); it("invokes program main on boot", async () => { - const { bootsharp, resources, Test } = await setup(); + const { bootsharp, resources, Program } = await setup(); await bootsharp.boot(resources); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); + expect(Program.onMainInvoked).toHaveBeenCalledOnce(); }); it("enables debugging when debugging resources are present", async () => { const { bootsharp, resources } = await setup(); @@ -73,7 +74,7 @@ describe("boot", () => { expect(config.globalizationMode).toStrictEqual("invariant"); }); it("fetches resources when root is specified", async () => { - const { bootsharp, resources, Test } = await setup(); + const { bootsharp, resources, Program } = await setup(); const bin = [...resources.assemblies!, ...resources.icu!, ...resources.symbols!, ...resources.pdb!]; const fetchSpy = vi.fn(url => { const name = url.substring(url.lastIndexOf("/") + 1); @@ -84,7 +85,7 @@ describe("boot", () => { global.fetch = fetchSpy; try { await bootsharp.boot("/bin"); } finally { global.fetch = fetch; } - expect(Test.Program.onMainInvoked).toHaveBeenCalled(); + expect(Program.onMainInvoked).toHaveBeenCalled(); expect(fetchSpy).toHaveBeenCalledWith("/bin/dotnet.native.wasm"); expect(fetchSpy).toHaveBeenCalledWith("/bin/Bootsharp.Common.wasm"); }); diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 23ef8276..a8841164 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -1,21 +1,22 @@ import { describe, it, beforeAll, expect, vi } from "vitest"; -import { Event, Test, bootRuntime } from "../cs"; +import { Event, bootRuntime } from "../cs"; +import { Platform, Static } from "../cs/Test/bin/bootsharp/generated/test.g.mjs"; +import { IExportedModule, IImportedModule, Modules, Registries, IRegistryProvider, TrackType } from "../cs/Test/bin/bootsharp/generated/test/library.g.mjs"; +import type { IBidirectional, IImportedInstanced, IImportedInnerInstanced, Record } from "../cs/Test/bin/bootsharp/generated/test/library.g.mjs"; -const TrackType = Test.Library.TrackType; - -class Imported implements Test.Library.IImportedInstanced { - onRecordChanged = new Event<[Test.Library.IImportedInstanced, Test.Library.Record | undefined]>(); - record: Test.Library.Record | undefined = { id: "initial-rec" }; +class Imported implements IImportedInstanced { + onRecordChanged = new Event<[IImportedInstanced, Record | undefined]>(); + record: Record | undefined = { id: "initial-rec" }; inner = new ImportedInner(); constructor(private arg: string) { } getInstanceArg() { return this.arg; } - async getRecordIdAsync(record: Test.Library.Record) { + async getRecordIdAsync(record: Record) { await new Promise(res => setTimeout(res, 1)); return record.id; } } -class ImportedInner implements Test.Library.IImportedInnerInstanced { +class ImportedInner implements IImportedInnerInstanced { onCountChanged = new Event<[number]>(); #count = 0; get count() { return this.#count; } @@ -23,95 +24,104 @@ class ImportedInner implements Test.Library.IImportedInnerInstanced { increment() { this.count++; } } +class BidirectionalJS implements IBidirectional { + onBiChanged = new Event<[IBidirectional]>(); + #bi: IBidirectional; + constructor() { this.#bi = this; } + get bi() { return this.#bi; } + set bi(value) { this.onBiChanged.broadcast(this.#bi = value); } + echoBi(bi: IBidirectional) { return bi; } +} + describe("while bootsharp is not booted", () => { it("throws when attempting to invoke C# APIs", () => { - expect(Test.Static.echoExported).throw(/Boot the runtime before invoking C# APIs/); + expect(Static.echoExported).throw(/Boot the runtime before invoking C# APIs/); }); }); describe("while bootsharp is booted", () => { beforeAll(bootRuntime); it("JS functions are unassigned by default", () => { - expect(Test.Platform.throwJS).toBeUndefined(); - expect(Test.Static.importedFunction).toBeUndefined(); - expect(Test.Static.echoImported).toBeUndefined(); - expect(Test.Static.echoImportedAsync).toBeUndefined(); - expect(Test.Library.Registries.createVehicle).toBeUndefined(); - expect(Test.Library.RegistryProvider.getRegistry).toBeUndefined(); - expect(Test.Library.RegistryProvider.getRegistries).toBeUndefined(); - expect(Test.Library.RegistryProvider.getRegistryMap).toBeUndefined(); - expect(Test.Library.ImportedModule.getInstanceAsync).toBeUndefined(); + expect(Platform.throwJS).toBeUndefined(); + expect(Static.importedFunction).toBeUndefined(); + expect(Static.echoImported).toBeUndefined(); + expect(Static.echoImportedAsync).toBeUndefined(); + expect(Registries.createVehicle).toBeUndefined(); + expect(IRegistryProvider.getRegistry).toBeUndefined(); + expect(IRegistryProvider.getRegistries).toBeUndefined(); + expect(IRegistryProvider.getRegistryMap).toBeUndefined(); + expect(IImportedModule.getInstanceAsync).toBeUndefined(); }); it("errs when invoking unassigned imported function", () => { - expect(() => Test.Static.invokeImportedFunction()) + expect(() => Static.invokeImportedFunction()) .throw(/Failed to invoke '.+' from C#. Make sure to assign the function in JavaScript/); }); it("can invoke assigned imported function", () => { - Test.Static.importedFunction = vi.fn(); - Test.Static.invokeImportedFunction(); - expect(Test.Static.importedFunction).toHaveBeenCalledOnce(); + Static.importedFunction = vi.fn(); + Static.invokeImportedFunction(); + expect(Static.importedFunction).toHaveBeenCalledOnce(); }); it("can interop with imported statics", async () => { let prop = "initial imported"; - Test.Static.importedProperty = { get: () => prop, set: v => prop = v }; - Test.Static.echoImported = bytes => bytes; - Test.Static.echoImportedAsync = async bytes => { + Static.importedProperty = { get: () => prop, set: v => prop = v }; + Static.echoImported = bytes => bytes; + Static.echoImportedAsync = async bytes => { await new Promise(res => setTimeout(res, 1)); return bytes; }; - const promise = Test.Static.canInteropWithImportedStaticsAsync(); - Test.Static.importedEvent.broadcast("event payload"); + const promise = Static.canInteropWithImportedStaticsAsync(); + Static.importedEvent.broadcast("event payload"); await promise; }); it("can interop with exported statics", async () => { - expect(Test.Static.echoExported(new Uint8Array([2, 4])).reduce((s, i) => s + i, 0)).toStrictEqual(6); - expect((await Test.Static.echoExportedAsync(new Uint8Array([4, 2]))).reduce((s, i) => s + i, 0)).toStrictEqual(6); - expect(Test.Static.exportedProperty).toStrictEqual("initial exported"); - Test.Static.exportedProperty = "set"; - expect(Test.Static.exportedProperty).toStrictEqual("set"); + expect(Static.echoExported(new Uint8Array([2, 4])).reduce((s, i) => s + i, 0)).toStrictEqual(6); + expect((await Static.echoExportedAsync(new Uint8Array([4, 2]))).reduce((s, i) => s + i, 0)).toStrictEqual(6); + expect(Static.exportedProperty).toStrictEqual("initial exported"); + Static.exportedProperty = "set"; + expect(Static.exportedProperty).toStrictEqual("set"); const handler = vi.fn(); - Test.Static.exportedEvent.subscribe(handler); - Test.Static.broadcastExportedEvent("foo"); + Static.exportedEvent.subscribe(handler); + Static.broadcastExportedEvent("foo"); expect(handler).toHaveBeenCalledWith("foo"); - Test.Static.broadcastExportedEvent(undefined); + Static.broadcastExportedEvent(undefined); expect(handler).toHaveBeenCalledWith(undefined); - Test.Static.exportedEvent.unsubscribe(handler); - Test.Static.broadcastExportedEvent("bar"); + Static.exportedEvent.unsubscribe(handler); + Static.broadcastExportedEvent("bar"); expect(handler).not.toHaveBeenCalledWith("bar"); }); it("can interop with imported modules", async () => { - let record: Test.Library.Record | undefined = { id: "initial" }; - Test.Library.ImportedModule.record = { get: () => record, set: v => record = v }; - Test.Library.ImportedModule.getInstanceAsync = async (arg) => { + let record: Record | undefined = { id: "initial" }; + IImportedModule.record = { get: () => record, set: v => record = v }; + IImportedModule.getInstanceAsync = async (arg) => { await new Promise(res => setTimeout(res, 1)); return new Imported(arg); }; - const promise = Test.Library.Modules.canInteropWithImportedModuleAsync(); - Test.Library.ImportedModule.onRecordChanged.broadcast({ id: "event-rec" }); + const promise = Modules.canInteropWithImportedModuleAsync(); + IImportedModule.onRecordChanged.broadcast({ id: "event-rec" }); await promise; }); it("can interop with exported modules", async () => { const handler = vi.fn(); - Test.Library.ExportedModule.onRecordChanged.subscribe(handler); - Test.Library.ExportedModule.record = { id: "set" }; - expect(Test.Library.ExportedModule.record).toStrictEqual({ id: "set" }); + IExportedModule.onRecordChanged.subscribe(handler); + IExportedModule.record = { id: "set" }; + expect(IExportedModule.record).toStrictEqual({ id: "set" }); expect(handler).toHaveBeenCalledWith({ id: "set" }); - Test.Library.ExportedModule.record = undefined; - expect(Test.Library.ExportedModule.record).toBeUndefined(); + IExportedModule.record = undefined; + expect(IExportedModule.record).toBeUndefined(); expect(handler).toHaveBeenCalledWith(undefined); - const inst = await Test.Library.ExportedModule.getInstanceAsync("module-arg"); + const inst = await IExportedModule.getInstanceAsync("module-arg"); expect(inst.getInstanceArg()).toStrictEqual("module-arg"); - Test.Library.ExportedModule.onRecordChanged.unsubscribe(handler); + IExportedModule.onRecordChanged.unsubscribe(handler); }); it("can interop with imported instances", async () => { - Test.Library.ImportedModule.getInstanceAsync = async (arg) => new Imported(arg); + IImportedModule.getInstanceAsync = async (arg) => new Imported(arg); const imported = new Imported("instance-arg"); - const promise = Test.Library.Modules.canInteropWithImportedInstanceAsync(imported); + const promise = Modules.canInteropWithImportedInstanceAsync(imported); imported.onRecordChanged.broadcast(imported, { id: "event-rec" }); await promise; }); it("can interop with exported instances", async () => { - const exported = await Test.Library.ExportedModule.getInstanceAsync("instance-arg"); + const exported = await IExportedModule.getInstanceAsync("instance-arg"); const handler = vi.fn(); expect(exported.getInstanceArg()).toStrictEqual("instance-arg"); expect(await exported.getRecordIdAsync({ id: "rec" })).toStrictEqual("rec"); @@ -126,11 +136,30 @@ describe("while bootsharp is booted", () => { exported.onRecordChanged.unsubscribe(handler); }); it("can interop with imported inner instances", () => { - Test.Library.Modules.canInteropWithImportedInnerInstance(new Imported("")); + Modules.canInteropWithImportedInnerInstance(new Imported("")); + }); + it("can interop with bidirectional instances", () => { + const factory = () => new BidirectionalJS(); + Modules.importBi = factory; + expect(Modules.importBi).toBe(factory); + const exp = Modules.exportBi(); + const js = new BidirectionalJS(); + const handler = vi.fn(); + exp.onBiChanged.subscribe(handler); + expect(exp.echoBi(exp)).toBe(exp); + expect(exp.echoBi(js)).toBe(js); + exp.bi = js; + expect(handler).toHaveBeenCalledWith(js); + expect(exp.bi).toBe(js); + exp.bi = exp; + expect(handler).toHaveBeenCalledWith(exp); + expect(exp.bi).toBe(exp); + exp.onBiChanged.unsubscribe(handler); + Modules.canInteropWithBidirectional(); }); it("can interop with exported inner instances", async () => { const handler = vi.fn(); - const inner = (await Test.Library.ExportedModule.getInstanceAsync("bar")).inner; + const inner = (await IExportedModule.getInstanceAsync("bar")).inner; inner.onCountChanged.subscribe(handler); inner.count = 0; expect(handler).toHaveBeenCalledWith(0); @@ -140,9 +169,9 @@ describe("while bootsharp is booted", () => { expect(inner.count).toStrictEqual(2); }); it("releases instances after use", async () => { - Test.Library.ImportedModule.getInstanceAsync = async (arg) => new Imported(arg); - expect(await Test.Library.Modules.getImportedArgsAndFinalize("qux", "fox")).toStrictEqual(["qux", "fox"]); - expect(await Test.Library.Modules.getImportedArgsAndFinalize("zip", "zap")).toStrictEqual(["zip", "zap"]); + IImportedModule.getInstanceAsync = async (arg) => new Imported(arg); + expect(await Modules.getImportedArgsAndFinalize("qux", "fox")).toStrictEqual(["qux", "fox"]); + expect(await Modules.getImportedArgsAndFinalize("zip", "zap")).toStrictEqual(["zip", "zap"]); }); it("can echo records", () => { const expected = { @@ -155,21 +184,21 @@ describe("while bootsharp is booted", () => { { id: "tractor", trackType: TrackType.Rubber, maxSpeed: Math.fround(15.9) } ] }; - const actual = Test.Library.Registries.echoRegistry(expected); + const actual = Registries.echoRegistry(expected); expect(actual).toStrictEqual(expected); }); it("empty string of a record is transferred correctly", () => { - const id = Test.Library.Registries.getVehicleWithEmptyId().id; + const id = Registries.getVehicleWithEmptyId().id; expect(id).not.toBeNull(); expect(id).not.toBeUndefined(); expect(id).toStrictEqual(""); }); it("can transfer lists as arrays", async () => { - Test.Library.RegistryProvider.getRegistries = () => [{ + IRegistryProvider.getRegistries = () => [{ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], tracked: [] }]; - const result = await Test.Library.Registries.concatRegistriesAsync([ + const result = await Registries.concatRegistriesAsync([ { wheeled: [{ id: "bar", maxSpeed: 1, wheelCount: 9 }], tracked: [] }, { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }], wheeled: [] } ]); @@ -180,11 +209,11 @@ describe("while bootsharp is booted", () => { ]); }); it("can transfer dictionaries as maps", async () => { - Test.Library.RegistryProvider.getRegistryMap = () => new Map([ + IRegistryProvider.getRegistryMap = () => new Map([ ["foo", { wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], tracked: [] }], ["bar", { wheeled: [{ id: "bar", maxSpeed: 15, wheelCount: 5 }], tracked: [] }] ]); - const result = await Test.Library.Registries.mapRegistriesAsync(new Map([ + const result = await Registries.mapRegistriesAsync(new Map([ ["baz", { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }], wheeled: [] }] ])); expect(result).toStrictEqual(new Map([ @@ -194,48 +223,57 @@ describe("while bootsharp is booted", () => { ])); }); it("can invoke assigned JS functions in C#", () => { - Test.Library.RegistryProvider.getRegistry = () => ({ + IRegistryProvider.getRegistry = () => ({ wheeled: [{ id: "", maxSpeed: 1, wheelCount: 0 }], tracked: [{ id: "", maxSpeed: 2, trackType: TrackType.Chain }] }); - expect(Test.Library.Registries.countTotalSpeed()).toStrictEqual(3); + expect(Registries.countTotalSpeed()).toStrictEqual(3); }); it("can invoke assigned JS functions from a non-entry assembly", () => { - Test.Library.Registries.createVehicle = (id, maxSpeed) => ({ id, maxSpeed }); - expect(Test.Library.Registries.getVehicle("foo", 42)).toStrictEqual({ id: "foo", maxSpeed: 42 }); + Registries.createVehicle = (id, maxSpeed) => ({ id, maxSpeed }); + expect(Registries.getVehicle("foo", 42)).toStrictEqual({ id: "foo", maxSpeed: 42 }); }); it("can subscribe to events from a non-entry assembly", () => { const handler = vi.fn(); - Test.Library.Registries.onVehicleBroadcast.subscribe(handler); - Test.Library.Registries.broadcastVehicle({ id: "foo", maxSpeed: 42 }); + Registries.onVehicleBroadcast.subscribe(handler); + Registries.broadcastVehicle({ id: "foo", maxSpeed: 42 }); expect(handler).toHaveBeenCalledWith({ id: "foo", maxSpeed: 42 }); - Test.Library.Registries.broadcastVehicle(undefined); + Registries.broadcastVehicle(undefined); expect(handler).toHaveBeenCalledWith(undefined); }); + it("can interop with cs registry", () => { + const reg = Registries.makeRegistry(); + const wheeled = [{ id: "car", maxSpeed: 100, wheelCount: 4 }]; + const tracked = [{ id: "tank", maxSpeed: 20, trackType: TrackType.Chain }]; + reg.wheeled = wheeled; + reg.tracked = tracked; + expect(reg.wheeled).toStrictEqual(wheeled); + expect(reg.tracked).toStrictEqual(tracked); + }); it("can catch js exception", () => { - Test.Platform.throwJS = function () { throw new Error("foo"); }; - expect(Test.Platform.catchException()!.split("\n")[0]).toStrictEqual("Error: foo"); + Platform.throwJS = function () { throw new Error("foo"); }; + expect(Platform.catchException()!.split("\n")[0]).toStrictEqual("Error: foo"); }); it("can catch dotnet exceptions", () => { - expect(() => Test.Platform.throwCS("bar")).throw("bar"); + expect(() => Platform.throwCS("bar")).throw("bar"); }); it("can catch dotnet exceptions from async methods", async () => { - await expect(Test.Platform.throwCSAsync("baz")).rejects.toThrow("baz"); + await expect(Platform.throwCSAsync("baz")).rejects.toThrow("baz"); }); it("maps enums by both indexes and strings", () => { - expect(Test.Static.Enum[1]).toStrictEqual("One"); - expect(Test.Static.Enum[2]).toStrictEqual("Two"); - expect(Test.Static.Enum[Test.Static.Enum.One]).toStrictEqual("One"); - expect(Test.Static.Enum[Test.Static.Enum.Two]).toStrictEqual("Two"); + expect(Static.Enum[1]).toStrictEqual("One"); + expect(Static.Enum[2]).toStrictEqual("Two"); + expect(Static.Enum[Static.Enum.One]).toStrictEqual("One"); + expect(Static.Enum[Static.Enum.Two]).toStrictEqual("Two"); }); it("can compare indexed enums", () => { - expect(Test.Static.getEnum(1) === Test.Static.Enum.One).toBeTruthy(); - expect(Test.Static.getEnum(0) === Test.Static.Enum.One).not.toBeTruthy(); + expect(Static.getEnum(1) === Static.Enum.One).toBeTruthy(); + expect(Static.getEnum(0) === Static.Enum.One).not.toBeTruthy(); }); it("can transfer dates", () => { const date = new Date(1977, 3, 2); const expected = new Date(1977, 3, 9); - const actual = new Date(Test.Static.addDays(date, 7)); + const actual = new Date(Static.addDays(date, 7)); expect(actual).toStrictEqual(expected); }); }); diff --git a/src/js/test/spec/platform.spec.ts b/src/js/test/spec/platform.spec.ts index 70194df4..3cdb230f 100644 --- a/src/js/test/spec/platform.spec.ts +++ b/src/js/test/spec/platform.spec.ts @@ -1,20 +1,21 @@ import { describe, it, beforeAll, expect } from "vitest"; import { WebSocket, WebSocketServer } from "ws"; -import { Test, bootRuntime } from "../cs"; +import { bootRuntime } from "../cs"; +import { Platform } from "../cs/Test/bin/bootsharp/generated/test.g.mjs"; describe("platform", () => { beforeAll(bootRuntime); it("can provide unique guid", () => { - const guid1 = Test.Platform.getGuid(); - const guid2 = Test.Platform.getGuid(); + const guid1 = Platform.getGuid(); + const guid2 = Platform.getGuid(); expect(guid1.length).toStrictEqual(36); expect(guid2.length).toStrictEqual(36); expect(guid1).not.toStrictEqual(guid2); }); it("supports globalization", () => { - expect(Test.Platform.formatDate("ru", 5, 1, "d MMMM")).toStrictEqual("1 мая"); - expect(Test.Platform.formatDate("ja", 5, 1, "d MMMM")).toStrictEqual("1 5月"); - expect(Test.Platform.formatDate("en", 5, 1, "MMMM d")).toStrictEqual("May 1"); + expect(Platform.formatDate("ru", 5, 1, "d MMMM")).toStrictEqual("1 мая"); + expect(Platform.formatDate("ja", 5, 1, "d MMMM")).toStrictEqual("1 5月"); + expect(Platform.formatDate("en", 5, 1, "MMMM d")).toStrictEqual("May 1"); }); it("can communicate via websocket", async () => { // .NET requires ws package when running on node: @@ -24,7 +25,7 @@ describe("platform", () => { wss.on("connection", socket => socket.on("message", socket.send)); await new Promise(resolve => wss.once("listening", resolve)); const port = (wss.address() as { port: number }).port; - expect(await Test.Platform.echoWebSocket(`ws://localhost:${port}`, "foo", 3000)).toStrictEqual("foo"); + expect(await Platform.echoWebSocket(`ws://localhost:${port}`, "foo", 3000)).toStrictEqual("foo"); wss.close(); }); }); diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index d4c08550..1c2fd067 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -1,10 +1,13 @@ import { beforeAll, describe, expect, it } from "vitest"; -import { Test, bootRuntime } from "../cs"; +import { bootRuntime } from "../cs"; +import { Serialization } from "../cs/Test/bin/bootsharp/generated/test.g.mjs"; +import type { Primitives, Union } from "../cs/Test/bin/bootsharp/generated/test.g.mjs"; +import { Registries, IRegistryProvider, TrackType } from "../cs/Test/bin/bootsharp/generated/test/library.g.mjs"; describe("serialization", () => { beforeAll(bootRuntime); it("can echo primitives", () => { - const input: Test.Primitives = { + const input: Primitives = { boolean: true, byte: 7, sByte: -7, @@ -30,16 +33,16 @@ describe("serialization", () => { nullableInt: 42, missingInt: -1 }; - const expected: Test.Primitives = { + const expected: Primitives = { ...input, emptyChar: "\0", missingChar: "\0" }; - expect(Test.Serialization.echoPrimitives([input, null])).toStrictEqual([expected, null]); - expect(Test.Serialization.echoPrimitives(undefined)).toBeNull(); + expect(Serialization.echoPrimitives([input, null])).toStrictEqual([expected, null]); + expect(Serialization.echoPrimitives(undefined)).toBeNull(); }); it("can echo primitives with all nullable fields omitted", () => { - const input: Test.Primitives = { + const input: Primitives = { boolean: false, byte: 0, sByte: 0, positiveSByte: 0, int16: 0, uInt16: 0, int32: 0, uInt32: 0, int64: 0n, uInt64: 0, intPtr: 0, @@ -47,70 +50,70 @@ describe("serialization", () => { char: "\0", emptyChar: "\0", missingChar: "\0", dateTime: new Date(0), dateTimeOffset: new Date(0) }; - expect(Test.Serialization.echoPrimitives([input])).toStrictEqual([input]); + expect(Serialization.echoPrimitives([input])).toStrictEqual([input]); }); it("can echo unions", () => { - const a: Test.Union = { shared: "A", a: { string: "*", map: new Map([["a", null], ["b", 7]]) } }; - const b: Test.Union = { shared: "B", b: { ints: [], strings: ["foo", "bar"], times: [new Date()] } }; - expect(Test.Serialization.echoUnions([a, b, null])).toStrictEqual([a, b, null]); - expect(Test.Serialization.echoUnions(undefined)).toBeNull(); + const a: Union = { shared: "A", a: { string: "*", map: new Map([["a", null], ["b", 7]]) } }; + const b: Union = { shared: "B", b: { ints: [], strings: ["foo", "bar"], times: [new Date()] } }; + expect(Serialization.echoUnions([a, b, null])).toStrictEqual([a, b, null]); + expect(Serialization.echoUnions(undefined)).toBeNull(); }); it("can echo unions with all nullable fields omitted", () => { - const a: Test.Union = { shared: "A", a: {} }; - const b: Test.Union = { shared: "B", b: { strings: ["x"], times: [new Date()] } }; - expect(Test.Serialization.echoUnions([a, b])).toStrictEqual([a, b]); + const a: Union = { shared: "A", a: {} }; + const b: Union = { shared: "B", b: { strings: ["x"], times: [new Date()] } }; + expect(Serialization.echoUnions([a, b])).toStrictEqual([a, b]); }); it("can echo vehicles", async () => { - expect(Test.Library.Registries.echoRecords([{ id: "foo" }, null])) + expect(Registries.echoRecords([{ id: "foo" }, null])) .toStrictEqual([{ id: "foo" }, null]); - expect(Test.Library.Registries.echoVehicles([{ id: "foo", maxSpeed: 1 }, null])) + expect(Registries.echoVehicles([{ id: "foo", maxSpeed: 1 }, null])) .toStrictEqual([{ id: "foo", maxSpeed: 1 }, null]); - expect(Test.Library.Registries.echoRegistry({ + expect(Registries.echoRegistry({ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 2 }, null], - tracked: [{ id: "bar", maxSpeed: 2, trackType: Test.Library.TrackType.Chain }, null] + tracked: [{ id: "bar", maxSpeed: 2, trackType: TrackType.Chain }, null] })).toStrictEqual({ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 2 }, null], - tracked: [{ id: "bar", maxSpeed: 2, trackType: Test.Library.TrackType.Chain }, null] + tracked: [{ id: "bar", maxSpeed: 2, trackType: TrackType.Chain }, null] }); - Test.Library.RegistryProvider.getRegistries = () => []; - await expect(Test.Library.Registries.concatRegistriesAsync([null])).resolves.toStrictEqual([null]); + IRegistryProvider.getRegistries = () => []; + await expect(Registries.concatRegistriesAsync([null])).resolves.toStrictEqual([null]); }); it("can echo arrays", () => { - expect(Test.Serialization.echoBytes(new Uint8Array([1, 2, 3]))).toStrictEqual(new Uint8Array([1, 2, 3])); - expect(Test.Serialization.echoIntArray(new Int32Array([-1, 0, 1]))).toStrictEqual(new Int32Array([-1, 0, 1])); - expect(Test.Serialization.echoDoubleArray(new Float64Array([0.5, -1.9]))).toStrictEqual(new Float64Array([0.5, -1.9])); - expect(Test.Serialization.echoStringArray(["a", null, "", "b"])).toStrictEqual(["a", null, "", "b"]); - expect(Test.Serialization.echoNullableIntArray([1, null, -1])).toStrictEqual([1, null, -1]); - expect(Test.Serialization.echoNestedIntArray([new Int32Array([-1, 2]), null, new Int32Array()])).toStrictEqual([new Int32Array([-1, 2]), null, new Int32Array()]); - expect(Test.Serialization.echoBytes(undefined)).toBeNull(); - expect(Test.Serialization.echoIntArray(undefined)).toBeNull(); - expect(Test.Serialization.echoDoubleArray(undefined)).toBeNull(); - expect(Test.Serialization.echoStringArray(undefined)).toBeNull(); - expect(Test.Serialization.echoNullableIntArray(undefined)).toBeNull(); - expect(Test.Serialization.echoNestedIntArray(undefined)).toBeNull(); + expect(Serialization.echoBytes(new Uint8Array([1, 2, 3]))).toStrictEqual(new Uint8Array([1, 2, 3])); + expect(Serialization.echoIntArray(new Int32Array([-1, 0, 1]))).toStrictEqual(new Int32Array([-1, 0, 1])); + expect(Serialization.echoDoubleArray(new Float64Array([0.5, -1.9]))).toStrictEqual(new Float64Array([0.5, -1.9])); + expect(Serialization.echoStringArray(["a", null, "", "b"])).toStrictEqual(["a", null, "", "b"]); + expect(Serialization.echoNullableIntArray([1, null, -1])).toStrictEqual([1, null, -1]); + expect(Serialization.echoNestedIntArray([new Int32Array([-1, 2]), null, new Int32Array()])).toStrictEqual([new Int32Array([-1, 2]), null, new Int32Array()]); + expect(Serialization.echoBytes(undefined)).toBeNull(); + expect(Serialization.echoIntArray(undefined)).toBeNull(); + expect(Serialization.echoDoubleArray(undefined)).toBeNull(); + expect(Serialization.echoStringArray(undefined)).toBeNull(); + expect(Serialization.echoNullableIntArray(undefined)).toBeNull(); + expect(Serialization.echoNestedIntArray(undefined)).toBeNull(); }); it("can echo lists", () => { - expect(Test.Serialization.echoIntList([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Test.Serialization.echoStringList(["a", null, "", "b"])).toStrictEqual(["a", null, "", "b"]); - expect(Test.Serialization.echoNestedIntList([[1, 2], null, []])).toStrictEqual([[1, 2], null, []]); - expect(Test.Serialization.echoListInterface([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Test.Serialization.echoReadOnlyList([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Test.Serialization.echoCollection([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Test.Serialization.echoReadOnlyCollection([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Test.Serialization.echoIntList(undefined)).toBeNull(); - expect(Test.Serialization.echoStringList(undefined)).toBeNull(); - expect(Test.Serialization.echoNestedIntList(undefined)).toBeNull(); + expect(Serialization.echoIntList([1, 2, 3])).toStrictEqual([1, 2, 3]); + expect(Serialization.echoStringList(["a", null, "", "b"])).toStrictEqual(["a", null, "", "b"]); + expect(Serialization.echoNestedIntList([[1, 2], null, []])).toStrictEqual([[1, 2], null, []]); + expect(Serialization.echoListInterface([1, 2, 3])).toStrictEqual([1, 2, 3]); + expect(Serialization.echoReadOnlyList([1, 2, 3])).toStrictEqual([1, 2, 3]); + expect(Serialization.echoCollection([1, 2, 3])).toStrictEqual([1, 2, 3]); + expect(Serialization.echoReadOnlyCollection([1, 2, 3])).toStrictEqual([1, 2, 3]); + expect(Serialization.echoIntList(undefined)).toBeNull(); + expect(Serialization.echoStringList(undefined)).toBeNull(); + expect(Serialization.echoNestedIntList(undefined)).toBeNull(); }); it("can echo dictionaries", () => { - expect(Test.Serialization.echoDictionary(new Map([["1", "a"], ["2", null], ["3", ""]]))) + expect(Serialization.echoDictionary(new Map([["1", "a"], ["2", null], ["3", ""]]))) .toStrictEqual(new Map([["1", "a"], ["2", null], ["3", ""]])); - expect(Test.Serialization.echoNestedDictionary([new Map([["1", "a"]]), null, new Map([["2", null]])])) + expect(Serialization.echoNestedDictionary([new Map([["1", "a"]]), null, new Map([["2", null]])])) .toStrictEqual([new Map([["1", "a"]]), null, new Map([["2", null]])]); - expect(Test.Serialization.echoDictionaryInterface(new Map([[1, 2], [3, 4], [5, 0]]))) + expect(Serialization.echoDictionaryInterface(new Map([[1, 2], [3, 4], [5, 0]]))) .toStrictEqual(new Map([[1, 2], [3, 4], [5, 0]])); - expect(Test.Serialization.echoReadOnlyDictionary(new Map([[1, 2], [3, 4], [5, 0]]))) + expect(Serialization.echoReadOnlyDictionary(new Map([[1, 2], [3, 4], [5, 0]]))) .toStrictEqual(new Map([[1, 2], [3, 4], [5, 0]])); - expect(Test.Serialization.echoDictionary(undefined)).toBeNull(); - expect(Test.Serialization.echoNestedDictionary(undefined)).toBeNull(); + expect(Serialization.echoDictionary(undefined)).toBeNull(); + expect(Serialization.echoNestedDictionary(undefined)).toBeNull(); }); }); From 4c8a3bf46f04004705d1ed1147661dcf620895e2 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 19 May 2026 17:54:58 +0300 Subject: [PATCH 02/13] fmt --- .../GenerateJS/JSModuleTest.cs | 12 ++++++------ .../Common/Inspection/TypeInspector.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index 2072d402..0e9742c9 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -639,12 +639,12 @@ public void DoesNotEmitObjectsForUnrelatedTypes () """ public record Record; public class Outer { public record NestedRecord; } - public class Class - { - public record InnerRecord; - [Export] public static void Foo (Record record) {} - [Export] public static void Bar (Outer.NestedRecord nested) {} - [Export] public static void Baz (InnerRecord inner) {} + public class Class + { + public record InnerRecord; + [Export] public static void Foo (Record record) {} + [Export] public static void Bar (Outer.NestedRecord nested) {} + [Export] public static void Baz (InnerRecord inner) {} } """)); Execute(); diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index 1b702212..e5df7909 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -57,7 +57,7 @@ public IReadOnlyCollection Collect () private ModuleMeta? InspectModule (Type type, InteropKind ik) { if (!inspectedModuleTypes.Add(type) || IsStatic(type) || - ik == InteropKind.Import && !type.IsInterface) return null; + (ik == InteropKind.Import && !type.IsInterface)) return null; var md = new ModuleMeta(type) { IK = ik, Proxy = BuildProxy(type, ik), From de5e083c0ac980643e5711a015b9d59440e37414 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 19 May 2026 18:35:00 +0300 Subject: [PATCH 03/13] fix samples --- samples/bench/bootsharp/init.mjs | 8 ++++---- samples/bench/bootsharp/package.json | 3 ++- samples/bench/readme.md | 2 +- samples/minimal/cs/package.json | 3 ++- samples/minimal/index.html | 4 ++-- samples/trimming/cs/Trimming.csproj | 2 +- samples/trimming/cs/package.json | 3 ++- src/cs/Bootsharp/Build/Bootsharp.targets | 4 ++-- src/cs/Directory.Build.props | 2 +- 9 files changed, 17 insertions(+), 14 deletions(-) diff --git a/samples/bench/bootsharp/init.mjs b/samples/bench/bootsharp/init.mjs index 73c62ef8..f4ada4ce 100644 --- a/samples/bench/bootsharp/init.mjs +++ b/samples/bench/bootsharp/init.mjs @@ -1,12 +1,12 @@ -import bootsharp, { Exported, Imported } from "./bin/bootsharp/index.mjs"; +import bootsharp, { IExported, IImported } from "./bin/bootsharp/index.mjs"; import { getNumber, getStruct } from "../fixtures.mjs"; /** @returns {Promise} */ export async function init() { - Imported.getNumber = getNumber; - Imported.getStruct = getStruct; + IImported.getNumber = getNumber; + IImported.getStruct = getStruct; await bootsharp.boot(import.meta.resolve("./bin/bootsharp/bin")); - return { ...Exported }; + return { ...IExported }; } diff --git a/samples/bench/bootsharp/package.json b/samples/bench/bootsharp/package.json index 2c5ec2ab..92794570 100644 --- a/samples/bench/bootsharp/package.json +++ b/samples/bench/bootsharp/package.json @@ -2,7 +2,8 @@ "name": "bootsharp", "type": "module", "exports": { - ".": "./bin/bootsharp/index.mjs" + ".": "./bin/bootsharp/index.mjs", + "./*": "./bin/bootsharp/generated/*.g.mjs" }, "browser": { "node:fs": false, diff --git a/samples/bench/readme.md b/samples/bench/readme.md index aad513c4..16d672a6 100644 --- a/samples/bench/readme.md +++ b/samples/bench/readme.md @@ -34,4 +34,4 @@ All results are relative to the Rust baseline (lower is better). |-------------|-------|-------|-----------|-----------|----------|---------| | Fibonacci | `1.0` | `2.0` | `1.2` | `1.2` | `2.1` | `6.0` | | Echo Number | `1.0` | `1.0` | `1.6` | `18.9` | `28.2` | `1068.1` | -| Echo Struct | `1.0` | `1.4` | `2.2` | `1.1` | `7.7` | `21.2` | +| Echo Struct | `1.0` | `1.4` | `2.2` | `0.9` | `7.7` | `21.2` | diff --git a/samples/minimal/cs/package.json b/samples/minimal/cs/package.json index 2c5ec2ab..92794570 100644 --- a/samples/minimal/cs/package.json +++ b/samples/minimal/cs/package.json @@ -2,7 +2,8 @@ "name": "bootsharp", "type": "module", "exports": { - ".": "./bin/bootsharp/index.mjs" + ".": "./bin/bootsharp/index.mjs", + "./*": "./bin/bootsharp/generated/*.g.mjs" }, "browser": { "node:fs": false, diff --git a/samples/minimal/index.html b/samples/minimal/index.html index ba35d96b..d37f3af8 100644 --- a/samples/minimal/index.html +++ b/samples/minimal/index.html @@ -5,7 +5,7 @@

Loading...