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