diff --git a/examples/semantic-kernel/example.js b/examples/semantic-kernel/example.js
index 084c991e..a52209bf 100644
--- a/examples/semantic-kernel/example.js
+++ b/examples/semantic-kernel/example.js
@@ -4,11 +4,12 @@
// @ts-check
import dotnet from 'node-api-dotnet';
+import './bin/System.Text.Encodings.Web.js';
+import './bin/Microsoft.Extensions.DependencyInjection.js';
import './bin/Microsoft.Extensions.Logging.Abstractions.js';
import './bin/Microsoft.SemanticKernel.Abstractions.js';
import './bin/Microsoft.SemanticKernel.Core.js';
-import './bin/Microsoft.SemanticKernel.Connectors.AI.OpenAI.js';
-import './bin/Microsoft.SemanticKernel.TemplateEngine.Basic.js';
+import './bin/Microsoft.SemanticKernel.Connectors.OpenAI.js';
const Logging = dotnet.Microsoft.Extensions.Logging;
const SK = dotnet.Microsoft.SemanticKernel;
@@ -28,18 +29,22 @@ const loggerFactory = {
dispose() {}
};
-let kernelBuilder = new SK.KernelBuilder();
-kernelBuilder.WithLoggerFactory(loggerFactory);
+const kernelBuilder = SK.Kernel.CreateBuilder();
+//kernelBuilder.WithLoggerFactory(loggerFactory);
// The JS marshaller does not yet support extension methods.
-SK.OpenAIKernelBuilderExtensions.WithAzureOpenAIChatCompletionService(
+SK.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(
kernelBuilder,
process.env['OPENAI_DEPLOYMENT'] || '',
process.env['OPENAI_ENDPOINT'] || '',
process.env['OPENAI_KEY'] || '',
+ // Include optional parameters to disambiguate the overload.
+ undefined,
+ undefined,
+ undefined,
);
-const kernel = kernelBuilder.Build();
+const kernel = SK.KernelExtensions.Build(kernelBuilder);
const prompt = `{{$input}}
@@ -57,15 +62,18 @@ such orders would conflict with the First Law.
does not conflict with the First or Second Law.
`;
-const requestSettings = new SK.Connectors.AI.OpenAI.OpenAIRequestSettings();
-requestSettings.MaxTokens = 100;
+const executionSettings = new SK.Connectors.OpenAI.OpenAIPromptExecutionSettings();
+executionSettings.MaxTokens = 100;
// The JS marshaller does not yet support extension methods.
-const summaryFunction = SK.OpenAIKernelExtensions
- .CreateSemanticFunction(kernel, prompt, requestSettings);
+const summaryFunction = SK.KernelExtensions.CreateFunctionFromPrompt(
+ kernel, prompt, executionSettings);
-const summary = await SK.SKFunctionExtensions.InvokeAsync(
- summaryFunction, textToSummarize, kernel);
+const summarizeArguments = new Map();
+summarizeArguments.set('input', textToSummarize);
+
+const summary = await kernel.InvokeAsync(
+ summaryFunction, new SK.KernelArguments(summarizeArguments, undefined));
console.log();
console.log(summary.toString());
diff --git a/examples/semantic-kernel/semantic-kernel.csproj b/examples/semantic-kernel/semantic-kernel.csproj
index 04c96616..0b91556d 100644
--- a/examples/semantic-kernel/semantic-kernel.csproj
+++ b/examples/semantic-kernel/semantic-kernel.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/src/NodeApi.DotNetHost/JSMarshaller.cs b/src/NodeApi.DotNetHost/JSMarshaller.cs
index e59296c1..2efc0c23 100644
--- a/src/NodeApi.DotNetHost/JSMarshaller.cs
+++ b/src/NodeApi.DotNetHost/JSMarshaller.cs
@@ -351,7 +351,7 @@ public Expression BuildFromJSConstructorExpression(ConstructorInfo c
for (int i = 0; i < parameters.Length; i++)
{
- argVariables[i] = Expression.Variable(parameters[i].ParameterType, parameters[i].Name);
+ argVariables[i] = Variable(parameters[i]);
statements.Add(Expression.Assign(argVariables[i],
BuildArgumentExpression(i, parameters[i])));
}
@@ -1859,10 +1859,10 @@ private LambdaExpression BuildConvertFromJSValueExpression(Type toType)
// public type is passed to JS and then passed back to .NET as `object` type.
/*
- * (T)(value.TryUnwrap() ?? value.TryGetValueExternal());
+ * (T)(value.TryUnwrap() ?? value.GetValueExternalOrPrimitive());
*/
- MethodInfo getExternalMethod =
- typeof(JSNativeApi).GetStaticMethod(nameof(JSNativeApi.TryGetValueExternal));
+ MethodInfo getExternalMethod = typeof(JSNativeApi).GetStaticMethod(
+ nameof(JSNativeApi.GetValueExternalOrPrimitive));
statements = new[]
{
Expression.Convert(
diff --git a/src/NodeApi.DotNetHost/ManagedHost.cs b/src/NodeApi.DotNetHost/ManagedHost.cs
index 87c77f74..4eba004c 100644
--- a/src/NodeApi.DotNetHost/ManagedHost.cs
+++ b/src/NodeApi.DotNetHost/ManagedHost.cs
@@ -175,12 +175,14 @@ JSValue removeListener(JSCallbackArgs args)
}
public static bool IsTracingEnabled { get; } =
+ Debugger.IsAttached ||
Environment.GetEnvironmentVariable("TRACE_NODE_API_HOST") == "1";
public static void Trace(string msg)
{
if (IsTracingEnabled)
{
+ Debug.WriteLine(msg);
Console.WriteLine(msg);
Console.Out.Flush();
}
@@ -213,7 +215,8 @@ public static napi_value InitializeModule(napi_env env, napi_value exports)
JSRuntime runtime = new NodejsRuntime();
- if (Environment.GetEnvironmentVariable("TRACE_NODE_API_RUNTIME") != null)
+ if (Debugger.IsAttached ||
+ Environment.GetEnvironmentVariable("TRACE_NODE_API_RUNTIME") != null)
{
TraceSource trace = new(typeof(JSValue).Namespace!);
trace.Switch.Level = SourceLevels.All;
diff --git a/src/NodeApi.DotNetHost/TypeExporter.cs b/src/NodeApi.DotNetHost/TypeExporter.cs
index 672b6fa9..4f98baca 100644
--- a/src/NodeApi.DotNetHost/TypeExporter.cs
+++ b/src/NodeApi.DotNetHost/TypeExporter.cs
@@ -94,6 +94,9 @@ private JSReference ExportType(Type type)
private JSReference ExportClass(Type type)
{
+ string typeName = type.Name;
+ Trace($"### ExportClass({typeName}");
+
if (_exportedTypes.TryGetValue(type, out JSReference? classObjectReference))
{
return classObjectReference;
@@ -101,55 +104,68 @@ private JSReference ExportClass(Type type)
Trace($"> {nameof(TypeExporter)}.ExportClass({type.FormatName()})");
- bool isStatic = type.IsAbstract && type.IsSealed;
- Type classBuilderType =
- (type.IsValueType ? typeof(JSStructBuilder<>) : typeof(JSClassBuilder<>))
- .MakeGenericType(isStatic ? typeof(object) : type);
-
- object classBuilder;
- if (type.IsInterface || isStatic || type.IsValueType)
- {
- classBuilder = classBuilderType.CreateInstance(
- new[] { typeof(string) }, new[] { type.Name });
- }
- else
+ // Add a temporary null entry to the dictionary while exporting this type, in case the
+ // type is encountered while exporting members. It will be non-null by the time this method returns
+ // (or removed if an exception is thrown).
+ _exportedTypes.Add(type, null!);
+ try
{
- ConstructorInfo[] constructors =
- type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
- .Where(IsSupportedConstructor)
- .ToArray();
- JSCallbackDescriptor constructorDescriptor;
- if (constructors.Length == 1 &&
- !constructors[0].GetParameters().Any((p) => p.IsOptional))
+ bool isStatic = type.IsAbstract && type.IsSealed;
+ Type classBuilderType =
+ (type.IsValueType ? typeof(JSStructBuilder<>) : typeof(JSClassBuilder<>))
+ .MakeGenericType(isStatic ? typeof(object) : type);
+
+ object classBuilder;
+ if (type.IsInterface || isStatic || type.IsValueType)
{
- constructorDescriptor =
- _marshaller.BuildFromJSConstructorExpression(constructors[0]).Compile();
+ classBuilder = classBuilderType.CreateInstance(
+ new[] { typeof(string) }, new[] { type.Name });
}
else
{
- // Multiple constructors or optional parameters require overload resolution.
- constructorDescriptor =
- _marshaller.BuildConstructorOverloadDescriptor(constructors);
- }
+ ConstructorInfo[] constructors =
+ type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
+ .Where(IsSupportedConstructor)
+ .ToArray();
+ JSCallbackDescriptor constructorDescriptor;
+ if (constructors.Length == 1 &&
+ !constructors[0].GetParameters().Any((p) => p.IsOptional))
+ {
+ constructorDescriptor =
+ _marshaller.BuildFromJSConstructorExpression(constructors[0]).Compile();
+ }
+ else
+ {
+ // Multiple constructors or optional parameters require overload resolution.
+ constructorDescriptor =
+ _marshaller.BuildConstructorOverloadDescriptor(constructors);
+ }
- classBuilder = classBuilderType.CreateInstance(
- new[] { typeof(string), typeof(JSCallbackDescriptor) },
- new object[] { type.Name, constructorDescriptor });
- }
+ classBuilder = classBuilderType.CreateInstance(
+ new[] { typeof(string), typeof(JSCallbackDescriptor) },
+ new object[] { type.Name, constructorDescriptor });
+ }
- ExportProperties(type, classBuilder);
- ExportMethods(type, classBuilder);
- ExportNestedTypes(type, classBuilder);
+ ExportProperties(type, classBuilder);
+ ExportMethods(type, classBuilder);
+ ExportNestedTypes(type, classBuilder);
- string defineMethodName = type.IsInterface ? "DefineInterface" :
- isStatic ? "DefineStaticClass" : type.IsValueType ? "DefineStruct" : "DefineClass";
- MethodInfo defineClassMethod = classBuilderType.GetInstanceMethod(defineMethodName);
- JSValue classObject = (JSValue)defineClassMethod.Invoke(
- classBuilder,
- defineClassMethod.GetParameters().Select((_) => (object?)null).ToArray())!;
+ string defineMethodName = type.IsInterface ? "DefineInterface" :
+ isStatic ? "DefineStaticClass" : type.IsValueType ? "DefineStruct" : "DefineClass";
+ MethodInfo defineClassMethod = classBuilderType.GetInstanceMethod(defineMethodName);
+ JSValue classObject = (JSValue)defineClassMethod.Invoke(
+ classBuilder,
+ defineClassMethod.GetParameters().Select((_) => (object?)null).ToArray())!;
- classObjectReference = new JSReference(classObject);
- _exportedTypes.Add(type, classObjectReference);
+ classObjectReference = new JSReference(classObject);
+ _exportedTypes[type] = classObjectReference;
+ }
+ catch
+ {
+ // Clean up the temporary null entry.
+ _exportedTypes.Remove(type);
+ throw;
+ }
// Also export any types returned by properties or methods of this type, because
// they might otherwise not be referenced by JS before they are used.
diff --git a/src/NodeApi.Generator/SourceGenerator.cs b/src/NodeApi.Generator/SourceGenerator.cs
index dabe1d57..49737449 100644
--- a/src/NodeApi.Generator/SourceGenerator.cs
+++ b/src/NodeApi.Generator/SourceGenerator.cs
@@ -24,6 +24,8 @@ public abstract class SourceGenerator
private static readonly Regex s_paragraphBreakRegex = new(@" *\ *");
+ protected const char NonBreakingSpace = (char)0xA0;
+
public enum DiagnosticId
{
NoExports = 1000,
@@ -193,11 +195,11 @@ protected static IEnumerable WrapComment(string comment, int wrapColumn)
}
}
- yield return comment.Substring(0, i).TrimEnd();
+ yield return comment.Substring(0, i).TrimEnd().Replace(NonBreakingSpace, ' ');
comment = comment.Substring(i + 1);
}
- yield return comment.TrimEnd();
+ yield return comment.TrimEnd().Replace(NonBreakingSpace, ' ');
}
}
}
diff --git a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs
index 9fe2127f..ec762cfd 100644
--- a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs
+++ b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs
@@ -134,33 +134,25 @@ public static void GenerateTypeDefinitions(
bool isSystemAssembly = false,
bool suppressWarnings = false)
{
- // Create a metadata load context that includes a resolver for .NET system assemblies
- // along with the target assembly.
-
- // Resolve all assemblies in all the system reference assembly directories.
- string[] systemAssemblies = systemReferenceAssemblyDirectories
- .SelectMany((d) => Directory.GetFiles(d, "*.dll"))
- .ToArray();
-
- // Drop reference assemblies that are already in any system ref assembly directories.
- // (They would only support older framework versions.)
- referenceAssemblyPaths = referenceAssemblyPaths.Where(
- (r) => !systemAssemblies.Any((a) => Path.GetFileName(a).Equals(
- Path.GetFileName(r), StringComparison.OrdinalIgnoreCase)));
-
+ // Create a metadata load context that includes a resolver for system assemblies,
+ // referenced assemblies, referenced assemblies, and the target assembly.
+ IEnumerable allReferenceAssemblyPaths = MergeSystemReferenceAssemblies(
+ referenceAssemblyPaths, systemReferenceAssemblyDirectories);
PathAssemblyResolver assemblyResolver = new(
- new[] { typeof(object).Assembly.Location }
- .Concat(systemAssemblies)
- .Concat(referenceAssemblyPaths)
- .Append(assemblyPath));
- using MetadataLoadContext loadContext = new(
- assemblyResolver, typeof(object).Assembly.GetName().Name);
+ allReferenceAssemblyPaths.Append(assemblyPath));
+ using MetadataLoadContext loadContext = new(assemblyResolver);
Assembly assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
Dictionary referenceAssemblies = new();
foreach (string referenceAssemblyPath in referenceAssemblyPaths)
{
+ if (!allReferenceAssemblyPaths.Contains(referenceAssemblyPath))
+ {
+ // The referenced assembly was replaced by a system assembly.
+ continue;
+ }
+
Assembly referenceAssembly = loadContext.LoadFromAssemblyPath(referenceAssemblyPath);
string referenceAssemblyName = referenceAssembly.GetName().Name!;
referenceAssemblies.Add(referenceAssemblyName, referenceAssembly);
@@ -226,6 +218,59 @@ public static void GenerateTypeDefinitions(
}
}
+ ///
+ /// Finds system assemblies that may be referenced by project code, and resolves
+ /// conflicts between project-referenced assemblies and system assemblies by selecting the
+ /// highest version of each assembly.
+ ///
+ private static IEnumerable MergeSystemReferenceAssemblies(
+ IEnumerable referenceAssemblyPaths,
+ IEnumerable systemReferenceAssemblyDirectories)
+ {
+ // Resolve all assemblies in all the system reference assembly directories.
+ IEnumerable systemAssemblyPaths = systemReferenceAssemblyDirectories
+ .SelectMany((d) => Directory.GetFiles(d, "*.dll"));
+
+ // Concatenate system reference assemblies with project (nuget) reference assemblies.
+ IEnumerable allAssemblyPaths = new[] { typeof(object).Assembly.Location }
+ .Concat(systemAssemblyPaths)
+ .Concat(referenceAssemblyPaths);
+
+ // Select the latest version of each referenced assembly.
+ // First group by assembly name, then pick the highest version in each group.
+ IEnumerable> assembliesByVersion = allAssemblyPaths.Concat(referenceAssemblyPaths)
+ .GroupBy(a => Path.GetFileNameWithoutExtension(a).ToLowerInvariant());
+ IEnumerable mergedAssemblyPaths = assembliesByVersion.Select(
+ (g) => g.OrderByDescending((a) => InferReferenceAssemblyVersionFromPath(a)).First());
+ return mergedAssemblyPaths;
+ }
+
+ private static Version InferReferenceAssemblyVersionFromPath(string assemblyPath)
+ {
+ var pathParts = assemblyPath.Split(
+ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).ToList();
+
+ // Infer the version from a system reference assembly path such as
+ // dotnet\packs\Microsoft.NETCore.App.Ref\\ref\net6.0\AssemblyName.dll
+ int refIndex = pathParts.IndexOf("ref");
+ if (refIndex > 0 && Version.TryParse(pathParts[refIndex - 1], out Version? refVersion))
+ {
+ return refVersion;
+ }
+
+ // Infer the version from a nuget package assembly reference path such as
+ // \\lib\net6.0\AssemblyName.dll
+ int libIndex = pathParts.IndexOf("lib");
+ if (libIndex > 0 && Version.TryParse(pathParts[libIndex - 1], out Version? libVersion))
+ {
+ return libVersion;
+ }
+
+ // The version cannot be inferred from the path. The reference will still be used
+ // if it is the only one with that assembly name.
+ return new Version();
+ }
+
public TypeDefinitionsGenerator(
Assembly assembly,
XDocument? assemblyDoc,
@@ -570,8 +615,11 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
foreach (ConstructorInfo constructor in type.GetConstructors(
BindingFlags.Public | BindingFlags.Instance))
{
- if (isFirstMember) isFirstMember = false; else s++;
- ExportTypeMember(ref s, constructor);
+ if (!IsExcludedMember(constructor))
+ {
+ if (isFirstMember) isFirstMember = false; else s++;
+ ExportTypeMember(ref s, constructor);
+ }
}
if (type.IsClass)
@@ -579,14 +627,17 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
foreach (PropertyInfo property in type.GetProperties(
BindingFlags.Public | BindingFlags.Static))
{
- if (isFirstMember) isFirstMember = false; else s++;
- ExportTypeMember(ref s, property);
+ if (!IsExcludedMember(property))
+ {
+ if (isFirstMember) isFirstMember = false; else s++;
+ ExportTypeMember(ref s, property);
+ }
}
foreach (MethodInfo method in type.GetMethods(
BindingFlags.Public | BindingFlags.Static))
{
- if (!IsExcludedMethod(method))
+ if (!IsExcludedMember(method))
{
if (isFirstMember) isFirstMember = false; else s++;
ExportTypeMember(ref s, method);
@@ -660,8 +711,11 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
foreach (ConstructorInfo constructor in type.GetConstructors(
BindingFlags.Public | BindingFlags.Instance))
{
- if (isFirstMember) isFirstMember = false; else s++;
- ExportTypeMember(ref s, constructor);
+ if (!IsExcludedMember(constructor))
+ {
+ if (isFirstMember) isFirstMember = false; else s++;
+ ExportTypeMember(ref s, constructor);
+ }
}
}
@@ -672,8 +726,11 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
(isStaticClass ? BindingFlags.DeclaredOnly : default) |
(type.IsInterface || isGenericTypeDefinition ? default : BindingFlags.Static)))
{
- if (isFirstMember) isFirstMember = false; else s++;
- ExportTypeMember(ref s, property);
+ if (!IsExcludedMember(property))
+ {
+ if (isFirstMember) isFirstMember = false; else s++;
+ ExportTypeMember(ref s, property);
+ }
}
foreach (MethodInfo method in type.GetMethods(
@@ -681,7 +738,7 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
(isStaticClass ? BindingFlags.DeclaredOnly : default) |
(type.IsInterface || isGenericTypeDefinition ? default : BindingFlags.Static)))
{
- if (!IsExcludedMethod(method))
+ if (!IsExcludedMember(method))
{
if (isFirstMember) isFirstMember = false; else s++;
ExportTypeMember(ref s, method);
@@ -701,11 +758,11 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
private static bool HasExplicitInterfaceImplementations(Type type, Type interfaceType)
{
- if (!type.IsClass)
+ if (type.IsInterface)
{
- if ((interfaceType.Name == nameof(IComparable) && type.IsInterface &&
+ if ((interfaceType.Name == nameof(IComparable) &&
type.GetInterfaces().Any((i) => i.Name == typeof(IComparable<>).Name)) ||
- (interfaceType.Name == "ISpanFormattable" && type.IsInterface &&
+ (interfaceType.Name == "ISpanFormattable" &&
(type.Name == "INumberBase`1" ||
type.GetInterfaces().Any((i) => i.Name == "INumberBase`1"))))
{
@@ -732,21 +789,28 @@ private static bool HasExplicitInterfaceImplementations(Type type, Type interfac
interfaceType = interfaceType.GetGenericTypeDefinition();
}
- // Get the interface type name with generic type parameters for matching.
+ // Get the interface type name prefix for matching the method name.
// It would be more precise to match the generic type params also,
// but also more complicated.
- string interfaceTypeName = interfaceType.FullName!;
- int genericMarkerIndex = interfaceTypeName.IndexOf('`');
+ string methodNamePrefix = interfaceType.FullName!;
+ int genericMarkerIndex = methodNamePrefix.IndexOf('`');
if (genericMarkerIndex >= 0)
{
- interfaceTypeName = interfaceTypeName.Substring(0, genericMarkerIndex);
+#if NETFRAMEWORK
+ methodNamePrefix = methodNamePrefix.Substring(0, genericMarkerIndex) + '<';
+#else
+ methodNamePrefix = string.Concat(methodNamePrefix.AsSpan(0, genericMarkerIndex), "<");
+#endif
+ }
+ else
+ {
+ methodNamePrefix += '.';
}
foreach (MethodInfo method in type.GetMethods(
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
- if (method.IsFinal && method.IsPrivate &&
- method.Name.StartsWith(interfaceTypeName))
+ if (method.IsFinal && method.IsPrivate && method.Name.StartsWith(methodNamePrefix))
{
return true;
}
@@ -905,17 +969,47 @@ private void EndNamespace(ref SourceBuilder s, Type type)
}
}
- private static bool IsExcludedMethod(MethodInfo method)
+ private static bool IsExcludedMember(PropertyInfo property)
+ {
+ if (property.PropertyType.IsPointer)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsExcludedMember(MethodBase method)
{
// Exclude "special" methods like property get/set and event add/remove.
+ if (method is MethodInfo && method.IsSpecialName)
+ {
+ return true;
+ }
+
// Exclude old style Begin/End async methods, as they always have Task-based alternatives.
- // Exclude instance methods declared by System.Object like ToString() and Equals().
- return method.IsSpecialName ||
- (method.Name.StartsWith("Begin") &&
- method.ReturnType.FullName == typeof(IAsyncResult).FullName) ||
+ if ((method.Name.StartsWith("Begin") &&
+ (method as MethodInfo)?.ReturnType.FullName == typeof(IAsyncResult).FullName) ||
(method.Name.StartsWith("End") && method.GetParameters().Length == 1 &&
- method.GetParameters()[0].ParameterType.FullName == typeof(IAsyncResult).FullName) ||
- (!method.IsStatic && method.DeclaringType!.FullName == "System.Object");
+ method.GetParameters()[0].ParameterType.FullName == typeof(IAsyncResult).FullName))
+ {
+ return true;
+ }
+
+ // Exclude instance methods declared by System.Object like ToString() and Equals().
+ if (!method.IsStatic && method.DeclaringType!.FullName == "System.Object")
+ {
+ return true;
+ }
+
+ // Exclude methods that have pointer parameters because they can't be marshalled to JS.
+ if (method.GetParameters().Any((p) => p.ParameterType.IsPointer) ||
+ method is MethodInfo { ReturnParameter.ParameterType.IsPointer: true })
+ {
+ return true;
+ }
+
+ return false;
}
private void GenerateEnumDefinition(ref SourceBuilder s, Type type)
@@ -1461,7 +1555,7 @@ private void GenerateDocComments(
if (string.IsNullOrEmpty(remarks) && summary.Length < 83 && summary.IndexOf('\n') < 0)
{
- s += $"/** {summary} */";
+ s += $"/** {summary.Replace(NonBreakingSpace, ' ')} */";
}
else
{
@@ -1494,9 +1588,9 @@ private static string FormatDocText(XNode? node)
if (node is XElement element)
{
- if (element.Name == "see")
+ if (element.Name == "see" && element.Attribute("cref") != null)
{
- string target = element.Attribute("cref")?.Value?.ToString() ?? string.Empty;
+ string target = element.Attribute("cref")!.Value;
target = target.Substring(target.IndexOf(':') + 1);
int genericCountIndex = target.LastIndexOf('`');
@@ -1510,16 +1604,27 @@ private static string FormatDocText(XNode? node)
target += $"<{new string(',', genericCount - 1)}>";
}
+ // Use a non-breaking space char to prevent wrapping from breaking the link.
+ // It will be replaced with by a regular space char in the final output.
+ return $"{{@link {target}}}".Replace(' ', NonBreakingSpace);
+ }
+ else if (element.Name == "see" && element.Attribute("langword") != null)
+ {
+ string target = element.Attribute("langword")!.Value;
return $"`{target}`";
}
- else if (element.Name == "paramref")
+ else if (element.Name == "paramref" && element.Attribute("name") != null)
{
- string target = element.Attribute("name")?.Value?.ToString() ?? string.Empty;
+ string target = element.Attribute("name")!.Value;
return $"`{target}`";
}
else
{
- return string.Join(" ", element.Nodes().Select(FormatDocText));
+ return string.Join(" ", element.Nodes().Select(FormatDocText))
+ .Replace("} ,", "},")
+ .Replace("} .", "}.")
+ .Replace("` ,", "`,")
+ .Replace("` .", "`.");
}
}
diff --git a/src/NodeApi/Native/JSNativeApi.cs b/src/NodeApi/Native/JSNativeApi.cs
index 302a6d0a..cc271d88 100644
--- a/src/NodeApi/Native/JSNativeApi.cs
+++ b/src/NodeApi/Native/JSNativeApi.cs
@@ -611,6 +611,27 @@ public static unsafe object GetValueExternal(this JSValue thisValue)
return GCHandle.FromIntPtr(result).Target!;
}
+ ///
+ /// Gets the .NET external value or primitive object value (string, boolean, or double)
+ /// for a JS value, or null if the JS value is not convertible to one of those types.
+ ///
+ ///
+ /// This is useful when marshalling where a JS value must be converted to some .NET type,
+ /// but the target type is unknown (object).
+ ///
+ public static unsafe object? GetValueExternalOrPrimitive(this JSValue thisValue)
+ {
+ return thisValue.TypeOf() switch
+ {
+ JSValueType.String => thisValue.GetValueStringUtf16(),
+ JSValueType.Boolean => thisValue.GetValueBool(),
+ JSValueType.Number => thisValue.GetValueDouble(),
+ JSValueType.External => thisValue.GetValueExternal(),
+ _ => null,
+ };
+
+ }
+
public static JSReference CreateReference(this JSValue thisValue)
=> new(thisValue);
diff --git a/src/NodeApi/Runtime/TracingJSRuntime.cs b/src/NodeApi/Runtime/TracingJSRuntime.cs
index cd346676..966850ba 100644
--- a/src/NodeApi/Runtime/TracingJSRuntime.cs
+++ b/src/NodeApi/Runtime/TracingJSRuntime.cs
@@ -2000,7 +2000,7 @@ public override napi_status DefineProperties(
return status;
}
- public override napi_status DefineClass(
+ public override unsafe napi_status DefineClass(
napi_env env,
string name,
napi_callback constructor,
@@ -2016,11 +2016,30 @@ public override napi_status DefineClass(
$"[{string.Join(", ", properties.ToArray().Select((p) => Format(env, p)))}]",
});
+ // Replace property callbacks with the tracing callbacks.
+ var tracedProperties = new napi_property_descriptor[properties.Length];
+ for (int i = 0; i < properties.Length; i++)
+ {
+ tracedProperties[i] = properties[i];
+ if (properties[i].getter == new napi_callback(JSNativeApi.s_invokeJSGetter))
+ {
+ tracedProperties[i].method = new napi_callback(s_traceGetterCallback);
+ }
+ if (properties[i].setter == new napi_callback(JSNativeApi.s_invokeJSSetter))
+ {
+ tracedProperties[i].method = new napi_callback(s_traceSetterCallback);
+ }
+ if (properties[i].method == new napi_callback(JSNativeApi.s_invokeJSMethod))
+ {
+ tracedProperties[i].method = new napi_callback(s_traceMethodCallback);
+ }
+ }
+
napi_status status;
try
{
status = _runtime.DefineClass(
- env, name, constructor, data, properties, out result);
+ env, name, constructor, data, tracedProperties, out result);
}
catch (Exception ex)
{
diff --git a/test/TypeDefsGeneratorTests.cs b/test/TypeDefsGeneratorTests.cs
index 1ff21415..adc41382 100644
--- a/test/TypeDefsGeneratorTests.cs
+++ b/test/TypeDefsGeneratorTests.cs
@@ -17,13 +17,20 @@ namespace Microsoft.JavaScript.NodeApi.Test;
public class TypeDefsGeneratorTests
{
private static TypeDefinitionsGenerator CreateTypeDefinitionsGenerator(
- Dictionary docs)
+ IEnumerable> docs)
+ {
+ return CreateTypeDefinitionsGenerator(docs.Select((pair) =>
+ new KeyValuePair(pair.Key, new XElement("summary", pair.Value))));
+ }
+
+ private static TypeDefinitionsGenerator CreateTypeDefinitionsGenerator(
+ IEnumerable> docs)
{
string ns = typeof(TypeDefsGeneratorTests).FullName + "+";
XDocument docsXml = new(new XElement("root", new XElement("members",
docs.Select((pair) => new XElement("member",
new XAttribute("name", pair.Key.Insert(2, ns)),
- new XElement("summary", pair.Value))))));
+ pair.Value)))));
return new TypeDefinitionsGenerator(
typeof(TypeDefsGeneratorTests).Assembly,
assemblyDoc: docsXml,
@@ -31,10 +38,16 @@ private static TypeDefinitionsGenerator CreateTypeDefinitionsGenerator(
suppressWarnings: true);
}
- private string GenerateTypeDefinition(Type type, Dictionary docs)
+ private string GenerateTypeDefinition(Type type, IDictionary docs)
=> CreateTypeDefinitionsGenerator(docs).GenerateTypeDefinition(type).TrimEnd();
- private string GenerateMemberDefinition(MemberInfo member, Dictionary docs)
+ private string GenerateMemberDefinition(MemberInfo member, IDictionary docs)
+ => CreateTypeDefinitionsGenerator(docs).GenerateMemberDefinition(member).TrimEnd();
+
+ private string GenerateTypeDefinition(Type type, IDictionary docs)
+ => CreateTypeDefinitionsGenerator(docs).GenerateTypeDefinition(type).TrimEnd();
+
+ private string GenerateMemberDefinition(MemberInfo member, IDictionary docs)
=> CreateTypeDefinitionsGenerator(docs).GenerateMemberDefinition(member).TrimEnd();
private interface SimpleInterface
@@ -267,6 +280,27 @@ export interface GenericDelegate$$1 {
["T:GenericDelegate`1"] = "generic-delegate",
}));
}
+
+ [Fact]
+ public void GenerateJSDocLink()
+ {
+ Assert.Equal("""
+
+ /** Link to {@link SimpleClass}. */
+ export interface SimpleInterface {
+ TestProperty: string;
+
+ TestMethod(): string;
+ }
+ """.ReplaceLineEndings(),
+ GenerateTypeDefinition(typeof(SimpleInterface), new Dictionary
+ {
+ ["T:SimpleInterface"] = new XElement("summary",
+ "Link to ",
+ new XElement("see", new XAttribute("cref", "SimpleClass")),
+ "."),
+ }));
+ }
}
#endif // !NETFRAMEWORK