Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Follow these steps exactly and sequentially whenever the Bootsharp package consu

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`
- If the current version does not already use an `-alpha.X` suffix, add one.
- Example: `0.8.0` -> `0.8.0-alpha.0` -> `0.8.0-alpha.1`.
- If the current version does not already use an `-alpha.X` suffix, add one.
- Example: `0.8.0` -> `0.8.0-alpha.0` -> `0.8.0-alpha.1`.
3. Package the C# library with `src/cs/.scripts/pack.sh` under `src/cs`.
4. Compile the end-to-end C# test projects with `npm run compile-test` under `src/js`.
5. Run the end-to-end JS tests with `npm run test` under `src/js`.
Expand All @@ -36,6 +36,19 @@ To check C# coverage, use `reportgenerator` on merged coverlet output. Example w

To check JS coverage, run `npm run cover` under `src/js`.

# Inspecting Generated Output

C# tests under `Bootsharp.Publish.Test` generate files inside a temporary `MockProject` root, which is deleted when the test is disposed. When you need to inspect the generated content, write it to a scratch file outside the mock project, for example:

```csharp
AddAssembly(With("// fixture source code"));
Execute();
File.WriteAllText(Path.Combine(Path.GetTempPath(), "scratch.txt"), GeneratedDeclarations);
Contains("// asserted generated content");
```

Then run the focused test, read the scratch file and remove the probe before finalizing. Do not commit debug dumps or temporary file writes.

# Running Shell Scripts

Always run `.sh` scripts with the `bash` command, for example: `bash script.sh`.
36 changes: 36 additions & 0 deletions docs/guide/declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,42 @@ Foo.onBar.subscribe(payload => {});

:::

## Documentation Declarations

When an inspected assembly has XML documentation generated, Bootsharp mirrors the matching documentation into the emitted TypeScript declarations.

::: code-group

```csharp [Foo.cs]
/// <summary>Math API.</summary>
public class MathApi
{
/// <summary>Adds two numbers.</summary>
/// <param name="left">Left number.</param>
/// <param name="right">Right number.</param>
/// <returns>The sum.</returns>
[JSInvokable]
public static int Add (int left, int right) => left + right;
}
```

```ts [bindings.d.ts]
/**
* Math API.
*/
export namespace MathApi {
/**
* Adds two numbers.
* @param left Left number.
* @param right Right number.
* @returns The sum.
*/
export function add(left: number, right: number): number;
}
```

:::

## Nullability

Bootsharp uses different TypeScript nullish forms depending on where a nullable C# value appears:
Expand Down
4 changes: 3 additions & 1 deletion src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public void Compile (IEnumerable<MockSource> sources, string assemblyPath)
{string.Join('\n', sources.Select(BuildSource))}
""";
var compilation = CreateCompilation(assemblyPath, source);
var result = compilation.Emit(assemblyPath);
using var assembly = File.Create(assemblyPath);
using var docs = File.Create(Path.ChangeExtension(assemblyPath, ".xml"));
var result = compilation.Emit(assembly, xmlDocumentationStream: docs);
if (result.Success) return;
var error = $"Invalid test source code: {result.Diagnostics.First().GetMessage()}";
Assert.Fail(string.Join('\n', [error, "---", source, "---"]));
Expand Down
145 changes: 145 additions & 0 deletions src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,4 +1192,149 @@ public class Class
Execute();
DoesNotContain("Foo");
}

[Fact]
public void GeneratesJsDocsOverCsDocs ()
{
AddAssembly(With(
"""
/// <summary>Payload kind.</summary>
public enum Kind
{
/// <summary>First kind.</summary>
First,
/// <summary>Second kind.</summary>
Second
}

/// <summary>A payload sent across interop.</summary>
/// <remarks>Visible in generated TypeScript.</remarks>
public record Payload
{
/// <summary>The payload name.</summary>
public string Name { get; init; }
}

/// <summary>Exported instance API.</summary>
public interface IExportedInstanced
{
/// <summary>Current state.</summary>
int State { get; }

/// <summary>Invokes instance.</summary>
/// <param name="value">Value to pass.</param>
void Inv (string value);
}

/// <summary>Static interop API.</summary>
public class Class
{
/// <summary>Runs foo.</summary>
/// <param name="function">Function value.</param>
/// <param name="names">Names to run.</param>
/// <returns>Computed value.</returns>
[JSInvokable] public static int Foo (List<int?> function, string[] names) => 0;

/// <summary>Gets payload.</summary>
[JSInvokable] public static Payload Get (Kind kind) => default;

/// <summary>Gets exported instance.</summary>
[JSInvokable] public static IExportedInstanced GetExported () => default;

/// <summary>Receives foo.</summary>
/// <param name="count">Count to receive.</param>
[JSFunction] public static void OnFoo (int count) { }

/// <param name="value">Value without summary.</param>
[JSFunction] public static void OnParamOnly (string value) { }

/// <summary>Signals completion.</summary>
/// <param name="done">Whether work is done.</param>
[JSEvent] public static void OnDone (bool done) { }
}
"""));
Execute();
Contains(
"""
/**
* Payload kind.
*/
export enum Kind {
/**
* First kind.
*/
First,
/**
* Second kind.
*/
Second
}
""");
Contains(
"""
/**
* A payload sent across interop.
*/
export interface Payload {
/**
* The payload name.
*/
name: string;
}
""");
Contains(
"""
/**
* Exported instance API.
*/
export interface IExportedInstanced {
/**
* Current state.
*/
readonly state: number;
/**
* Invokes instance.
* @param value Value to pass.
*/
inv(value: string): void;
}
""");
Contains(
"""
/**
* Static interop API.
*/
export namespace Class {
/**
* Runs foo.
* @param fn Function value.
* @param names Names to run.
* @returns Computed value.
*/
export function foo(fn: Array<number | null>, names: Array<string>): number;
/**
* Gets payload.
*/
export function get(kind: Kind): Payload;
/**
* Gets exported instance.
*/
export function getExported(): IExportedInstanced;
/**
* Receives foo.
* @param count Count to receive.
*/
export let onFoo: (count: number) => void;
/**
* @param value Value without summary.
*/
export let onParamOnly: (value: string) => void;
/**
* Signals completion.
* @param done Whether work is done.
*/
export const onDone: Event<[done: boolean]>;
}
""");
}
}
20 changes: 0 additions & 20 deletions src/cs/Bootsharp.Publish/Common/Meta/ArgumentMeta.cs

This file was deleted.

10 changes: 10 additions & 0 deletions src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Xml.Linq;

namespace Bootsharp.Publish;

/// <summary>
/// C# XML documentation generated for an inspected assembly.
/// </summary>
/// <param name="Assembly">Name of the assembly associated with the documentation.</param>
/// <param name="Xml">The XML documentation.</param>
internal sealed record DocumentationMeta (string Assembly, XDocument Xml);
40 changes: 38 additions & 2 deletions src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Bootsharp.Publish;

Expand All @@ -7,6 +8,10 @@ namespace Bootsharp.Publish;
/// </summary>
internal abstract record MemberMeta
{
/// <summary>
/// The reflected info of the member.
/// </summary>
public abstract MemberInfo Info { get; }
/// <summary>
/// Whether the member is implemented in C# and exposed to JavaScript (export)
/// or implemented in JavaScript and consumed from C# (import).
Expand Down Expand Up @@ -45,8 +50,12 @@ internal abstract record MemberMeta
/// <remarks>
/// Return value of the method is described in <see cref="MemberMeta.Value"/>.
/// </remarks>
internal record MethodMeta : MemberMeta
internal record MethodMeta (MethodInfo Info) : MemberMeta
{
/// <summary>
/// The reflected info of the method.
/// </summary>
public override MethodInfo Info { get; } = Info;
/// <summary>
/// Arguments of the method.
/// </summary>
Expand Down Expand Up @@ -80,8 +89,12 @@ internal sealed record EventMeta : MethodMeta
/// <summary>
/// An interop property declared on an interop interface.
/// </summary>
internal sealed record PropertyMeta : MemberMeta
internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta
{
/// <summary>
/// The reflected info of the property.
/// </summary>
public override PropertyInfo Info { get; } = Info;
/// <summary>
/// Whether the property has an accessible getter.
/// </summary>
Expand All @@ -91,3 +104,26 @@ internal sealed record PropertyMeta : MemberMeta
/// </summary>
public required bool CanSet { get; init; }
}

/// <summary>
/// Interop method argument.
/// </summary>
internal sealed record ArgumentMeta (ParameterInfo Info)
{
/// <summary>
/// The reflected info of the argument.
/// </summary>
public ParameterInfo Info { get; } = Info;
/// <summary>
/// C# name of the argument, as specified in source code.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// JavaScript name of the argument, to be specified in source code.
/// </summary>
public required string JSName { get; init; }
/// <summary>
/// Metadata of the argument's value.
/// </summary>
public required ValueMeta Value { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Bootsharp.Publish;

internal sealed class MemberInspector (Preferences prefs, TypeInspector types, SerializedInspector serde)
{
public PropertyMeta Inspect (PropertyInfo prop, InteropKind interop) => new() {
public PropertyMeta Inspect (PropertyInfo prop, InteropKind interop) => new(prop) {
Interop = interop,
Assembly = prop.DeclaringType!.Assembly.GetName().Name!,
Space = prop.DeclaringType.FullName!,
Expand All @@ -16,7 +16,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, S
CanSet = prop.SetMethod != null
};

public MethodMeta Inspect (MethodInfo method, InteropKind interop) => new() {
public MethodMeta Inspect (MethodInfo method, InteropKind interop) => new(method) {
Interop = interop,
Assembly = method.DeclaringType!.Assembly.GetName().Name!,
Space = method.DeclaringType.FullName!,
Expand All @@ -29,7 +29,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, S
Async = IsTaskLike(method.ReturnParameter.ParameterType)
};

private ArgumentMeta CreateArgument (ParameterInfo param) => new() {
private ArgumentMeta CreateArgument (ParameterInfo param) => new(param) {
Name = param.Name!,
JSName = param.Name == "function" ? "fn" : param.Name!,
Value = CreateValue(param.ParameterType, GetNullability(param))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable
/// </summary>
public required IReadOnlyCollection<SerializedMeta> Serialized { get; init; }
/// <summary>
/// C# XML documentation for the inspected assemblies.
/// </summary>
public required IReadOnlyCollection<DocumentationMeta> Documentation { get; init; }
/// <summary>
/// Warnings logged while inspecting the solution.
/// </summary>
public required IReadOnlyCollection<string> Warnings { get; init; }
Expand Down
Loading
Loading