diff --git a/AGENTS.md b/AGENTS.md
index f6141878..ab85b2e2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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`.
@@ -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`.
diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md
index f9f1e451..6148fff6 100644
--- a/docs/guide/declarations.md
+++ b/docs/guide/declarations.md
@@ -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]
+/// Math API.
+public class MathApi
+{
+ /// Adds two numbers.
+ /// Left number.
+ /// Right number.
+ /// The sum.
+ [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:
diff --git a/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs b/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs
index 25c22e9f..348cb6e5 100644
--- a/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs
+++ b/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs
@@ -21,7 +21,9 @@ public void Compile (IEnumerable 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, "---"]));
diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs
index b9360164..6b00d8a2 100644
--- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs
+++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs
@@ -1192,4 +1192,149 @@ public class Class
Execute();
DoesNotContain("Foo");
}
+
+ [Fact]
+ public void GeneratesJsDocsOverCsDocs ()
+ {
+ AddAssembly(With(
+ """
+ /// Payload kind.
+ public enum Kind
+ {
+ /// First kind.
+ First,
+ /// Second kind.
+ Second
+ }
+
+ /// A payload sent across interop.
+ /// Visible in generated TypeScript.
+ public record Payload
+ {
+ /// The payload name.
+ public string Name { get; init; }
+ }
+
+ /// Exported instance API.
+ public interface IExportedInstanced
+ {
+ /// Current state.
+ int State { get; }
+
+ /// Invokes instance.
+ /// Value to pass.
+ void Inv (string value);
+ }
+
+ /// Static interop API.
+ public class Class
+ {
+ /// Runs foo.
+ /// Function value.
+ /// Names to run.
+ /// Computed value.
+ [JSInvokable] public static int Foo (List function, string[] names) => 0;
+
+ /// Gets payload.
+ [JSInvokable] public static Payload Get (Kind kind) => default;
+
+ /// Gets exported instance.
+ [JSInvokable] public static IExportedInstanced GetExported () => default;
+
+ /// Receives foo.
+ /// Count to receive.
+ [JSFunction] public static void OnFoo (int count) { }
+
+ /// Value without summary.
+ [JSFunction] public static void OnParamOnly (string value) { }
+
+ /// Signals completion.
+ /// Whether work is done.
+ [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, names: Array): 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]>;
+ }
+ """);
+ }
}
diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ArgumentMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ArgumentMeta.cs
deleted file mode 100644
index 75cc65be..00000000
--- a/src/cs/Bootsharp.Publish/Common/Meta/ArgumentMeta.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Bootsharp.Publish;
-
-///
-/// Interop method argument.
-///
-internal sealed record ArgumentMeta
-{
- ///
- /// C# name of the argument, as specified in source code.
- ///
- public required string Name { get; init; }
- ///
- /// JavaScript name of the argument, to be specified in source code.
- ///
- public required string JSName { get; init; }
- ///
- /// Metadata of the argument's value.
- ///
- public required ValueMeta Value { get; init; }
-}
diff --git a/src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs
new file mode 100644
index 00000000..0229bec0
--- /dev/null
+++ b/src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs
@@ -0,0 +1,10 @@
+using System.Xml.Linq;
+
+namespace Bootsharp.Publish;
+
+///
+/// C# XML documentation generated for an inspected assembly.
+///
+/// Name of the assembly associated with the documentation.
+/// The XML documentation.
+internal sealed record DocumentationMeta (string Assembly, XDocument Xml);
diff --git a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs
index 29bf6d16..3596f1b7 100644
--- a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs
+++ b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
namespace Bootsharp.Publish;
@@ -7,6 +8,10 @@ namespace Bootsharp.Publish;
///
internal abstract record MemberMeta
{
+ ///
+ /// The reflected info of the member.
+ ///
+ 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).
@@ -45,8 +50,12 @@ internal abstract record MemberMeta
///
/// Return value of the method is described in .
///
-internal record MethodMeta : MemberMeta
+internal record MethodMeta (MethodInfo Info) : MemberMeta
{
+ ///
+ /// The reflected info of the method.
+ ///
+ public override MethodInfo Info { get; } = Info;
///
/// Arguments of the method.
///
@@ -80,8 +89,12 @@ internal sealed record EventMeta : MethodMeta
///
/// An interop property declared on an interop interface.
///
-internal sealed record PropertyMeta : MemberMeta
+internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta
{
+ ///
+ /// The reflected info of the property.
+ ///
+ public override PropertyInfo Info { get; } = Info;
///
/// Whether the property has an accessible getter.
///
@@ -91,3 +104,26 @@ internal sealed record PropertyMeta : MemberMeta
///
public required bool CanSet { get; init; }
}
+
+///
+/// Interop method argument.
+///
+internal sealed record ArgumentMeta (ParameterInfo Info)
+{
+ ///
+ /// The reflected info of the argument.
+ ///
+ public ParameterInfo Info { get; } = Info;
+ ///
+ /// C# name of the argument, as specified in source code.
+ ///
+ public required string Name { get; init; }
+ ///
+ /// JavaScript name of the argument, to be specified in source code.
+ ///
+ public required string JSName { get; init; }
+ ///
+ /// Metadata of the argument's value.
+ ///
+ public required ValueMeta Value { get; init; }
+}
diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs
index 4f94ffd2..2776a3a0 100644
--- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs
+++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs
@@ -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!,
@@ -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!,
@@ -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))
diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs
index 415eece7..4159aa11 100644
--- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs
+++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs
@@ -39,6 +39,10 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable
///
public required IReadOnlyCollection Serialized { 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; }
diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs
index 0002ec88..671a912f 100644
--- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs
+++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using System.Xml.Linq;
namespace Bootsharp.Publish;
@@ -7,6 +8,7 @@ internal sealed class SolutionInspector
private readonly List staticInterfaces = [];
private readonly List instancedInterfaces = [];
private readonly List staticMethods = [];
+ private readonly List docs = [];
private readonly List warnings = [];
private readonly TypeInspector typeInspector = new();
private readonly SerializedInspector serdeInspector = new();
@@ -36,8 +38,9 @@ public SolutionInspection Inspect (string directory, IEnumerable paths)
private void InspectAssemblyFile (string assemblyPath, MetadataLoadContext ctx)
{
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
- if (IsUserAssembly(assemblyName))
- InspectAssembly(ctx.LoadFromAssemblyPath(assemblyPath));
+ if (!IsUserAssembly(assemblyName)) return;
+ InspectDocumentation(assemblyPath, assemblyName);
+ InspectAssembly(ctx.LoadFromAssemblyPath(assemblyPath));
}
private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception)
@@ -54,9 +57,16 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception
StaticMethods = staticMethods.ToArray(),
Types = typeInspector.Collect(),
Serialized = serdeInspector.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 exported in assembly.GetExportedTypes())
diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs
new file mode 100644
index 00000000..f4039f12
--- /dev/null
+++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs
@@ -0,0 +1,90 @@
+using System.Reflection;
+using System.Text;
+using System.Xml.Linq;
+
+namespace Bootsharp.Publish;
+
+internal sealed class DocumentationBuilder (IReadOnlyCollection docs)
+{
+ public string BuildType (Type type, int indent)
+ {
+ var asm = type.Assembly.GetName().Name!;
+ var key = $"T:{GetXmlKey(type)}";
+ return GetXml(asm, key) is { } xml ? Build(GetSummary(xml), indent) : "";
+ }
+
+ public string BuildProperty (MemberInfo member, int indent)
+ {
+ 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) : "";
+ }
+
+ public string BuildFunction (MethodMeta method, int indent)
+ {
+ var asm = method.Info.DeclaringType!.Assembly.GetName().Name!;
+ if (GetXml(asm, GetMethodKey()) is not { } xml) return "";
+
+ var sum = GetSummary(xml);
+ foreach (var arg in method.Arguments)
+ if (xml.Elements("param").FirstOrDefault(e => e.Attribute("name")!.Value == arg.Info.Name) is { } x)
+ sum.Add($"@param {arg.JSName} {x.Value}");
+ if (xml.Element("returns") is { } returns)
+ sum.Add($"@returns {returns.Value}");
+ return Build(sum, indent);
+
+ string GetMethodKey ()
+ {
+ var key = new StringBuilder($"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();
+ }
+
+ string GetArgKey (Type type)
+ {
+ if (type.IsArray) return $"{GetArgKey(type.GetElementType()!)}[{new string(',', type.GetArrayRank() - 1)}]";
+ if (!type.IsGenericType) return GetXmlKey(type);
+ var definition = type.GetGenericTypeDefinition();
+ var name = definition.Name.Split('`')[0];
+ name = string.IsNullOrEmpty(definition.Namespace) ? name : $"{definition.Namespace}.{name}";
+ return $"{name}{{{string.Join(',', type.GetGenericArguments().Select(GetArgKey))}}}";
+ }
+ }
+
+ public string BuildEvent (EventMeta @event, int indent)
+ {
+ return BuildFunction(@event, indent);
+ }
+
+ private string Build (IReadOnlyList summary, int indent)
+ {
+ var pad = new string(' ', indent * 4);
+ var builder = new StringBuilder();
+ builder.Append($"\n{pad}/**");
+ foreach (var line in summary)
+ builder.Append($"\n{pad} * {line}");
+ builder.Append($"\n{pad} */");
+ return builder.ToString();
+ }
+
+ private static string GetXmlKey (Type type)
+ {
+ if (type.IsGenericType) type = type.GetGenericTypeDefinition();
+ if (type.IsNested) return $"{GetXmlKey(type.DeclaringType!)}.{type.Name}";
+ return string.IsNullOrEmpty(type.Namespace) ? type.Name : $"{type.Namespace}.{type.Name}";
+ }
+
+ 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);
+ }
+
+ private List GetSummary (XElement xml)
+ {
+ return xml.Elements("summary").Select(e => e.Value).ToList();
+ }
+}
diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs
index 7d4d0897..277f6471 100644
--- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs
+++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs
@@ -11,11 +11,13 @@ internal sealed class MemberDeclarationGenerator (Preferences prefs)
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 inspection)
{
+ docs = new(inspection.Documentation);
members = inspection.StaticMethods
.Concat(inspection.StaticInterfaces.SelectMany(i => i.Members))
.OrderBy(m => m.JSSpace).ToArray();
@@ -45,6 +47,7 @@ private bool ShouldOpenNamespace ()
private void OpenNamespace ()
{
+ builder.Append(docs.BuildType(member.Info.DeclaringType!, 0));
builder.Append($"\nexport namespace {member.JSSpace} {{");
}
@@ -61,6 +64,7 @@ private void CloseNamespace ()
private void DeclareProperty (PropertyMeta prop)
{
+ builder.Append(docs.BuildProperty(prop.Info, 1));
builder.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: ");
builder.Append(typeBuilder.Build(prop.Value.Type.Clr, prop.Value.Nullability));
if (prop.Value.Nullable) builder.Append(" | null");
@@ -69,6 +73,7 @@ private void DeclareProperty (PropertyMeta prop)
private void DeclareMethodExport (MethodMeta method)
{
+ builder.Append(docs.BuildFunction(method, 1));
builder.Append($"\n export function {method.JSName}(");
builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}"));
builder.Append($"): {typeBuilder.BuildReturn(method)};");
@@ -76,6 +81,7 @@ private void DeclareMethodExport (MethodMeta method)
private void DeclareMethodImport (MethodMeta method)
{
+ builder.Append(docs.BuildFunction(method, 1));
builder.Append($"\n export let {method.JSName}: (");
builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}"));
builder.Append($") => {typeBuilder.BuildReturn(method)};");
@@ -83,6 +89,7 @@ private void DeclareMethodImport (MethodMeta method)
private void DeclareEvent (EventMeta @event)
{
+ builder.Append(docs.BuildEvent(@event, 1));
builder.Append($"\n export const {@event.JSName}: Event<[");
builder.AppendJoin(", ", @event.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}"));
builder.Append("]>;");
diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs
index 95f397c3..11ef6987 100644
--- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs
+++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs
@@ -13,12 +13,14 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs)
private Type? nextType => index == types.Length - 1 ? null : GetTypeAt(index + 1);
private int indent => !string.IsNullOrEmpty(GetNamespace(type)) ? 1 : 0;
+ private DocumentationBuilder docs = null!;
private InterfaceMeta[] instanced = null!;
private Type[] types = null!;
private int index;
public string Generate (SolutionInspection inspection)
{
+ docs = new(inspection.Documentation);
instanced = [..inspection.InstancedInterfaces];
types = inspection.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray();
for (index = 0; index < types.Length; index++)
@@ -67,16 +69,21 @@ private void CloseNamespace ()
private void DeclareEnum ()
{
+ builder.Append(docs.BuildType(type, indent));
AppendLine($"export enum {type.Name} {{", indent);
var names = Enum.GetNames(type);
for (int i = 0; i < names.Length; i++)
+ {
+ builder.Append(docs.BuildProperty(type.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 DeclareInterface ()
{
+ builder.Append(docs.BuildType(type, indent));
AppendLine($"export interface {BuildTypeName(type)}", indent);
AppendExtensions();
builder.Append(" {");
@@ -106,7 +113,10 @@ private void AppendProperties ()
var flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance;
foreach (var prop in type.GetProperties(flags))
if (prop.GetMethod != null && prop.GetIndexParameters().Length == 0)
+ {
+ builder.Append(docs.BuildProperty(prop, indent + 1));
AppendProperty(ToFirstLower(prop.Name), prop.PropertyType, GetNullability(prop));
+ }
}
private void AppendProperty (string name, Type type, NullabilityInfo? nullability)
@@ -121,12 +131,14 @@ private void AppendProperty (string name, Type type, NullabilityInfo? nullabilit
private void AppendInstancedProperty (PropertyMeta prop)
{
+ builder.Append(docs.BuildProperty(prop.Info, indent + 1));
var name = prop.CanGet && !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName;
AppendProperty(name, prop.Value.Type.Clr, prop.Value.Nullability);
}
private void AppendInstancedEvent (EventMeta meta)
{
+ builder.Append(docs.BuildEvent(meta, indent + 1));
AppendLine(meta.JSName, indent + 1);
builder.Append(": Event<[");
builder.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}"));
@@ -135,6 +147,7 @@ private void AppendInstancedEvent (EventMeta meta)
private void AppendInstancedFunction (MethodMeta meta)
{
+ builder.Append(docs.BuildFunction(meta, indent + 1));
AppendLine(meta.JSName, indent + 1);
builder.Append('(');
builder.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}"));
diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props
index 6297c64e..d6b6ab34 100644
--- a/src/cs/Directory.Build.props
+++ b/src/cs/Directory.Build.props
@@ -1,7 +1,7 @@
- 0.8.0-alpha.90
+ 0.8.0-alpha.93
Elringus
javascript typescript ts js wasm node deno bun interop codegen
https://bootsharp.com
diff --git a/src/js/test/cs/Test/Invokable.cs b/src/js/test/cs/Test/Invokable.cs
index 5724b605..51011ea2 100644
--- a/src/js/test/cs/Test/Invokable.cs
+++ b/src/js/test/cs/Test/Invokable.cs
@@ -5,11 +5,16 @@
namespace Test;
+/// Invokable test API.
public static class Invokable
{
[JSInvokable]
public static void InvokeVoid () { }
+ /// Joins two strings.
+ /// First string.
+ /// Second string.
+ /// Joined string.
[JSInvokable]
public static string JoinStrings (string a, string b) => a + b;
diff --git a/src/js/test/cs/Test/Test.csproj b/src/js/test/cs/Test/Test.csproj
index c937d4a7..1f6f72fc 100644
--- a/src/js/test/cs/Test/Test.csproj
+++ b/src/js/test/cs/Test/Test.csproj
@@ -10,6 +10,8 @@
npx --yes rollup index.js -d ./ -f es -e process,module,fs/promises --output.preserveModules --entryFileNames [name].mjs --sourcemap
false
true
+ true
+ CS1591
diff --git a/src/js/test/spec/export.spec.ts b/src/js/test/spec/export.spec.ts
index 3a8eea5c..5d099dd4 100644
--- a/src/js/test/spec/export.spec.ts
+++ b/src/js/test/spec/export.spec.ts
@@ -15,4 +15,21 @@ describe("export", () => {
it("exports type declarations", () => {
expect(getDeclarations()).toBeTruthy();
});
+ it("exports documentation declarations", () => {
+ expect(getDeclarations()).toContain(`
+/**
+ * Invokable test API.
+ */
+export namespace Test.Invokable {
+ `);
+ expect(getDeclarations()).toContain(`
+ /**
+ * Joins two strings.
+ * @param a First string.
+ * @param b Second string.
+ * @returns Joined string.
+ */
+ export function joinStrings(a: string, b: string): string;
+ `);
+ });
});