Skip to content
Draft
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
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,13 @@ Useful references:

`Okojo.Annotations` and `Okojo.SourceGenerator` are for strongly-typed host APIs that should become JavaScript globals or generated object bindings.

Key export rules:

- `[GenerateJsGlobals]` exports members marked with `[JsMember]`, `[JsGlobalFunction]`, or `[JsGlobalProperty]`
- `[GenerateJsObject]` exports only members marked with `[JsMember]`
- `[JsIgnoreFromGlobals]` and `[JsIgnoreFromObject]` opt a shared member out of one surface
- if you do not specify an explicit JavaScript name, the generated export lowercases the first character (`Width` -> `width`, `SumNumbers` -> `sumNumbers`)

Example shape:

```csharp
Expand All @@ -377,16 +384,17 @@ using Okojo.DocGenerator.Annotations;
[DocDeclaration("globals")]
internal sealed partial class SketchGlobals
{
[JsGlobalProperty("width")]
[JsMember]
public int Width => 320;

[JsGlobalProperty("strokeWidth", Writable = true)]
[JsMember]
[JsGlobalProperty(Writable = true)]
public int StrokeWidth { get; set; } = 2;

[JsGlobalFunction("background")]
[JsMember("background")]
private void Background(byte r, byte g, byte b) { }

[JsGlobalFunction("sumNumbers")]
[JsMember]
private int SumNumbers(ReadOnlySpan<int> values) => values.ToArray().Sum();
}
```
Expand All @@ -408,7 +416,7 @@ Real references:

## Generated object bindings and doc annotations

`GenerateJsObjectAttribute` is for object-style bindings. `DocDeclarationAttribute` and `DocIgnoreAttribute` control declaration output.
`GenerateJsObjectAttribute` is for object-style bindings. Members are opt-in via `[JsMember]`. `DocDeclarationAttribute` and `DocIgnoreAttribute` control declaration output.

```csharp
using Okojo;
Expand All @@ -419,8 +427,10 @@ using Okojo.DocGenerator.Annotations;
[DocDeclaration("Foo/Bar", "Docs.Shapes")]
public partial class GeneratedHostBindingSample
{
[JsMember]
public float X { get; set; }

[JsMember]
public static int SumNumbers(ReadOnlySpan<int> values)
{
var sum = 0;
Expand All @@ -430,8 +440,10 @@ public partial class GeneratedHostBindingSample
}

[DocIgnore]
[JsMember]
public string Echo(string value) => $"echo:{value}";

[JsMember]
public static string DescribeJsValues(ReadOnlySpan<JsValue> values)
{
if (values.Length == 0)
Expand All @@ -441,6 +453,8 @@ public partial class GeneratedHostBindingSample
}
```

The generated JavaScript names for that sample are `x`, `sumNumbers`, `echo`, and `describeJsValues` unless you override them with `[JsMember("...")]`.

Real references:

- `examples/OkojoArtSandbox/GeneratedObjectSample.cs`
Expand Down
1 change: 1 addition & 0 deletions examples/OkojoArtSandbox/GeneratedObjectSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal partial class GeneratedObjectSample
public string Name { get; set; } = string.Empty;
public int Age { get; set; }

[JsMember]
public bool DoSomething()
{
return true;
Expand Down
1 change: 1 addition & 0 deletions src/Okojo.Annotations/GenerateJsGlobalsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public sealed class GenerateJsGlobalsAttribute : Attribute
{
public string InstallerMethodName { get; set; } = "InstallGeneratedGlobals";
public string PropertySourceMethodName { get; set; } = "GetGeneratedGlobalProperties";
public JsMemberNaming MemberNaming { get; set; } = JsMemberNaming.LowerCamelCase;
}
1 change: 1 addition & 0 deletions src/Okojo.Annotations/GenerateJsObjectAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace Okojo.Annotations;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class GenerateJsObjectAttribute : Attribute
{
public JsMemberNaming MemberNaming { get; set; } = JsMemberNaming.LowerCamelCase;
}
4 changes: 2 additions & 2 deletions src/Okojo.Annotations/JsGlobalFunctionAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Okojo.Annotations;

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class JsGlobalFunctionAttribute(string name) : Attribute
public sealed class JsGlobalFunctionAttribute(string? name = null) : Attribute
{
public string Name { get; } = name;
public string? Name { get; } = name;
public int Length { get; set; } = -1;
public bool IsConstructor { get; set; }
}
4 changes: 2 additions & 2 deletions src/Okojo.Annotations/JsGlobalPropertyAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Okojo.Annotations;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class JsGlobalPropertyAttribute(string name) : Attribute
public sealed class JsGlobalPropertyAttribute(string? name = null) : Attribute
{
public string Name { get; } = name;
public string? Name { get; } = name;
public bool Writable { get; set; }
public bool Enumerable { get; set; } = true;
public bool Configurable { get; set; } = true;
Expand Down
6 changes: 6 additions & 0 deletions src/Okojo.Annotations/JsIgnoreFromGlobalsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Okojo.Annotations;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public sealed class JsIgnoreFromGlobalsAttribute : Attribute
{
}
6 changes: 6 additions & 0 deletions src/Okojo.Annotations/JsIgnoreFromObjectAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Okojo.Annotations;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public sealed class JsIgnoreFromObjectAttribute : Attribute
{
}
7 changes: 7 additions & 0 deletions src/Okojo.Annotations/JsMemberAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Okojo.Annotations;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public sealed class JsMemberAttribute(string? name = null) : Attribute
{
public string? Name { get; } = name;
}
8 changes: 8 additions & 0 deletions src/Okojo.Annotations/JsMemberNaming.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Okojo.Annotations;

public enum JsMemberNaming
{
LowerCamelCase = 0,
PascalCase = 1,
AsDeclared = 2
}
17 changes: 14 additions & 3 deletions src/Okojo.Annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ Typical attributes include:

- `GenerateJsGlobalsAttribute`
- `GenerateJsObjectAttribute`
- `JsMemberAttribute`
- `JsGlobalFunctionAttribute`
- `JsGlobalPropertyAttribute`
- `JsIgnoreFromGlobalsAttribute`
- `JsIgnoreFromObjectAttribute`

## Typical use

Expand All @@ -17,21 +20,29 @@ using Okojo.Annotations;
[GenerateJsGlobals]
public static partial class ConsoleGlobals
{
[JsGlobalFunction]
public static string hello() => "hello";
[JsMember]
public static string Hello() => "hello";
}

[GenerateJsObject]
public partial class GeneratedObjectSample
{
[JsMember]
public string Name { get; set; } = string.Empty;
public int Age { get; set; }

[JsMember]
public bool DoSomething()
{
return true;
}
}
```

Notes:

- `[GenerateJsObject]` is explicit opt-in: only `[JsMember]` members are exported
- `[GenerateJsGlobals]` can use `[JsMember]` for shared naming/defaults, or `[JsGlobalFunction]` / `[JsGlobalProperty]` when you need function/property-specific options
- `[JsIgnoreFromGlobals]` and `[JsIgnoreFromObject]` let one member participate in only one export surface
- default generated JavaScript names lowercase the first character unless you provide an explicit name

Use `Okojo.SourceGenerator` when you want the Roslyn generator that turns these annotations into generated installers and export glue.
2 changes: 2 additions & 0 deletions src/Okojo.DocGenerator.Cli/Okojo.DocGenerator.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\Okojo.SourceGenerator\AttributeMetadataNames.cs" Link="Shared\AttributeMetadataNames.cs"/>
<Compile Include="..\Okojo.SourceGenerator\GlobalGeneration\GlobalGenerationNames.cs" Link="Shared\GlobalGenerationNames.cs"/>
<Compile Include="..\Okojo.SourceGenerator\GlobalGeneration\GlobalExportModels.cs" Link="Shared\GlobalExportModels.cs"/>
<Compile Include="..\Okojo.SourceGenerator\GlobalGeneration\GlobalExportCollector.cs" Link="Shared\GlobalExportCollector.cs"/>
<Compile Include="..\Okojo.SourceGenerator\JsExportAttributeHelper.cs" Link="Shared\JsExportAttributeHelper.cs"/>
<Compile Include="..\Okojo.SourceGenerator\ObjectGeneration\JsObjectExportModels.cs" Link="Shared\JsObjectExportModels.cs"/>
<Compile Include="..\Okojo.SourceGenerator\ObjectGeneration\JsObjectExportCollector.cs" Link="Shared\JsObjectExportCollector.cs"/>
<Compile Include="..\Okojo.SourceGenerator\ParameterGeneration\ParameterTypeSupport.cs" Link="Shared\ParameterTypeSupport.cs"/>
Expand Down
12 changes: 12 additions & 0 deletions src/Okojo.SourceGenerator/AttributeMetadataNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Okojo.SourceGenerator;

internal static class AttributeMetadataNames
{
public const string GenerateJsObjectAttribute = "Okojo.Annotations.GenerateJsObjectAttribute";
public const string GenerateJsGlobalsAttribute = "Okojo.Annotations.GenerateJsGlobalsAttribute";
public const string JsMemberAttribute = "Okojo.Annotations.JsMemberAttribute";
public const string JsIgnoreFromObjectAttribute = "Okojo.Annotations.JsIgnoreFromObjectAttribute";
public const string JsIgnoreFromGlobalsAttribute = "Okojo.Annotations.JsIgnoreFromGlobalsAttribute";
public const string JsGlobalFunctionAttribute = "Okojo.Annotations.JsGlobalFunctionAttribute";
public const string JsGlobalPropertyAttribute = "Okojo.Annotations.JsGlobalPropertyAttribute";
}
2 changes: 1 addition & 1 deletion src/Okojo.SourceGenerator/GenerateJsGlobalsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public sealed class GenerateJsGlobalsGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider.ForAttributeWithMetadataName(
GlobalGenerationNames.GenerateJsGlobalsAttribute,
AttributeMetadataNames.GenerateJsGlobalsAttribute,
static (node, _) => node is ClassDeclarationSyntax,
static (ctx, _) => GlobalExportCollector.Collect((INamedTypeSymbol)ctx.TargetSymbol));

Expand Down
26 changes: 12 additions & 14 deletions src/Okojo.SourceGenerator/GenerateJsObjectGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public sealed class GenerateJsObjectGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider.ForAttributeWithMetadataName(
"Okojo.Annotations.GenerateJsObjectAttribute",
AttributeMetadataNames.GenerateJsObjectAttribute,
static (node, _) => node is ClassDeclarationSyntax,
static (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol);

Expand Down Expand Up @@ -106,22 +106,23 @@ private static void EmitMembers(
switch (member.Symbol)
{
case IFieldSymbol field when !field.IsConst:
EmitField(sb, symbol, field);
EmitField(sb, symbol, member, field);
break;
case IPropertySymbol property when property.Parameters.Length == 0:
EmitProperty(sb, symbol, property);
EmitProperty(sb, symbol, member, property);
break;
}

foreach (var methodGroup in methodGroups)
EmitMethodBinding(sb, methodGroup, isStaticGroup);
}

private static void EmitField(StringBuilder sb, INamedTypeSymbol containingType, IFieldSymbol field)
private static void EmitField(StringBuilder sb, INamedTypeSymbol containingType, JsObjectMemberModel member,
IFieldSymbol field)
{
var fullTypeName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
sb.Append(" new global::Okojo.Runtime.Interop.HostMemberBinding(\"")
.Append(field.Name)
.Append(member.Name)
.Append("\", global::Okojo.Runtime.Interop.HostMemberBindingKind.Field, ")
.Append(field.IsStatic ? "true" : "false");
sb.Append(", getterBody: static (in global::Okojo.Runtime.CallInfo info) => ");
Expand All @@ -131,7 +132,7 @@ private static void EmitField(StringBuilder sb, INamedTypeSymbol containingType,
sb.Append('.').Append(field.Name);
});

if (!field.IsReadOnly)
if (member.CanWrite)
{
sb.Append(", setterBody: static (in global::Okojo.Runtime.CallInfo info) => { ");
AppendCallTarget(sb, fullTypeName, field.IsStatic);
Expand All @@ -143,14 +144,15 @@ private static void EmitField(StringBuilder sb, INamedTypeSymbol containingType,
sb.AppendLine("),");
}

private static void EmitProperty(StringBuilder sb, INamedTypeSymbol containingType, IPropertySymbol property)
private static void EmitProperty(StringBuilder sb, INamedTypeSymbol containingType, JsObjectMemberModel member,
IPropertySymbol property)
{
var fullTypeName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
sb.Append(" new global::Okojo.Runtime.Interop.HostMemberBinding(\"")
.Append(property.Name)
.Append(member.Name)
.Append("\", global::Okojo.Runtime.Interop.HostMemberBindingKind.Property, ")
.Append(property.IsStatic ? "true" : "false");
if (property.GetMethod is not null)
if (member.CanRead)
{
sb.Append(", getterBody: static (in global::Okojo.Runtime.CallInfo info) => ");
EmitToJsValue(sb, property.Type, () =>
Expand All @@ -160,7 +162,7 @@ private static void EmitProperty(StringBuilder sb, INamedTypeSymbol containingTy
});
}

if (property.SetMethod is not null)
if (member.CanWrite)
{
sb.Append(", setterBody: static (in global::Okojo.Runtime.CallInfo info) => { ");
AppendCallTarget(sb, fullTypeName, property.IsStatic);
Expand Down Expand Up @@ -215,10 +217,6 @@ private static void EmitMethodGroup(
"Host function argument type mismatch.",
methodGroup,
true,
static x => (IMethodSymbol)x.Symbol,
static x => x.Parameters,
static x => x.Type,
static _ => false,
overloadIndex => dispatcherName + "__Overload" + overloadIndex);

var fullTypeName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,6 @@ private static void EmitFunctionGroup(StringBuilder sb, INamedTypeSymbol contain
"Host function argument type mismatch.",
functionGroup,
false,
static x => x.Symbol,
static x => x.Parameters,
static x => x.Type,
static x => x.HasDefaultValue,
overloadIndex => dispatcherName + "__Overload" + overloadIndex.ToString(CultureInfo.InvariantCulture));

for (var i = 0; i < functionGroup.Overloads.Count; i++)
Expand Down
Loading