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