From da9d2c3a4537294eb38cd92b1e7c4d74fc3a0c22 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:16:52 +0300 Subject: [PATCH 1/4] update agents --- AGENTS.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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`. From e96b264ed0b78dec9b6a3cf930f00048117f7313 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:35:28 +0300 Subject: [PATCH 2/4] iter --- .../Mock/MockCompiler.cs | 4 +- .../Pack/DeclarationTest.cs | 145 ++++++++++++++++++ .../Common/Meta/ArgumentMeta.cs | 20 --- .../Common/Meta/DocumentationMeta.cs | 10 ++ .../Common/Meta/MemberMeta.cs | 40 ++++- .../SolutionInspector/MemberInspector.cs | 6 +- .../SolutionInspector/SolutionInspection.cs | 4 + .../SolutionInspector/SolutionInspector.cs | 14 +- .../DocumentationBuilder.cs | 90 +++++++++++ .../MemberDeclarationGenerator.cs | 7 + .../TypeDeclarationGenerator.cs | 13 ++ 11 files changed, 325 insertions(+), 28 deletions(-) delete mode 100644 src/cs/Bootsharp.Publish/Common/Meta/ArgumentMeta.cs create mode 100644 src/cs/Bootsharp.Publish/Common/Meta/DocumentationMeta.cs create mode 100644 src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs 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)}")); From 178b003c3c5235bb326031d380ebf64870b7174f Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:05:42 +0300 Subject: [PATCH 3/4] add js test --- src/cs/Directory.Build.props | 2 +- src/js/test/cs/Test/Invokable.cs | 5 +++++ src/js/test/cs/Test/Test.csproj | 2 ++ src/js/test/spec/export.spec.ts | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) 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; + `); + }); }); From d035763f873a176b07f7b10231452ea40517cefb Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:17:17 +0300 Subject: [PATCH 4/4] add docs --- docs/guide/declarations.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) 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: