diff --git a/src/NodeApi.DotNetHost/JSMarshaller.cs b/src/NodeApi.DotNetHost/JSMarshaller.cs index f9ba321e..185c600b 100644 --- a/src/NodeApi.DotNetHost/JSMarshaller.cs +++ b/src/NodeApi.DotNetHost/JSMarshaller.cs @@ -140,24 +140,6 @@ private string ToCamelCase(string name) return sb.ToString(); } - /// - /// Converts a value to a JS value. - /// - public JSValue From(T value) - { - JSValue.From converter = GetToJSValueDelegate(); - return converter(value); - } - - /// - /// Converts a JS value to a requested type. - /// - public T To(JSValue value) - { - JSValue.To converter = GetFromJSValueDelegate(); - return converter(value); - } - /// /// Checks whether a type is converted to a JavaScript built-in type. /// @@ -2238,7 +2220,7 @@ private LambdaExpression BuildConvertToJSValueExpression(Type fromType) { statements = new[] { valueParameter }; } - else if (fromType == typeof(object) || !fromType.IsPublic) + else if (fromType == typeof(object) || !(fromType.IsPublic || fromType.IsNestedPublic)) { // Marshal unknown or nonpublic type as external, so at least it can be round-tripped. Expression objectExpression = fromType.IsValueType ? diff --git a/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs b/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs index 2feb4f2f..16bf7067 100644 --- a/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs +++ b/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs @@ -15,42 +15,56 @@ public static class JSRuntimeContextExtensions /// /// Imports a module or module property from JavaScript and converts it to an interface. /// - /// Type of the value being imported. + /// .NET type that the imported JS value will be marshalled to. /// Name of the module being imported, or null to import a /// global property. This is equivalent to the value provided to import or /// require() in JavaScript. Required if is null. /// Name of a property on the module (or global), or null to import /// the module object. Required if is null. - /// The imported value. + /// JS marshaller instance to use to convert the imported value + /// to a .NET type. + /// The imported value, marshalled to the specified .NET type. /// Both and /// are null. public static T Import( this JSRuntimeContext runtimeContext, string? module, - string? property) + string? property, + bool esModule, + JSMarshaller marshaller) { - JSValue jsValue = runtimeContext.Import(module, property); - return JSMarshaller.Current.To(jsValue); + if (marshaller == null) throw new ArgumentNullException(nameof(marshaller)); + + JSValue jsValue = runtimeContext.Import(module, property, esModule); + return marshaller.FromJS(jsValue); } /// /// Imports a module or module property from JavaScript and converts it to an interface. /// - /// Type of the value being imported. + /// .NET type that the imported JS value will be marshalled to. /// Name of the module being imported, or null to import a /// global property. This is equivalent to the value provided to import or /// require() in JavaScript. Required if is null. /// Name of a property on the module (or global), or null to import /// the module object. Required if is null. - /// The imported value. + /// JS marshaller instance to use to convert the imported value + /// to a .NET type. + /// The imported value, marshalled to the specified .NET type. /// Both and /// are null. public static T Import( this NodejsEnvironment nodejs, string? module, - string? property) + string? property, + bool esModule, + JSMarshaller marshaller) { + if (marshaller == null) throw new ArgumentNullException(nameof(marshaller)); + JSValueScope scope = nodejs; - return scope.RuntimeContext.Import(module, property); + return scope.RuntimeContext.Import(module, property, esModule, marshaller); } + + // TODO: ImportAsync() } diff --git a/src/NodeApi.DotNetHost/ManagedHost.cs b/src/NodeApi.DotNetHost/ManagedHost.cs index 48c68705..4ac27471 100644 --- a/src/NodeApi.DotNetHost/ManagedHost.cs +++ b/src/NodeApi.DotNetHost/ManagedHost.cs @@ -52,15 +52,6 @@ public sealed class ManagedHost : JSEventEmitter, IDisposable private JSValueScope? _rootScope; - /// - /// Strong reference to the JS object that is the exports for this module. - /// - /// - /// The exports object has module APIs such as `require()` and `load()`, along with - /// top-level .NET namespaces like `System` and `Microsoft`. - /// - private readonly JSReference _exports; - /// /// Component that dynamically exports types from loaded assemblies. /// @@ -140,10 +131,8 @@ JSValue removeListener(JSCallbackArgs args) AutoCamelCase = false, }; - // Save the exports object, on which top-level namespaces will be defined. - _exports = new JSReference(exports); - - _typeExporter = new() + // The type exporter will define top-level namespace properties on the exports object. + _typeExporter = new(JSMarshaller.Current, exports) { // Delay-loading is enabled by default, but can be disabled with this env variable. IsDelayLoadEnabled = @@ -151,13 +140,13 @@ JSValue removeListener(JSCallbackArgs args) }; // Export the System.Runtime and System.Console assemblies by default. - _typeExporter.ExportAssemblyTypes(typeof(object).Assembly, exports); + _typeExporter.ExportAssemblyTypes(typeof(object).Assembly); _loadedAssembliesByName.Add( typeof(object).Assembly.GetName().Name!, typeof(object).Assembly); if (typeof(Console).Assembly != typeof(object).Assembly) { - _typeExporter.ExportAssemblyTypes(typeof(Console).Assembly, exports); + _typeExporter.ExportAssemblyTypes(typeof(Console).Assembly); _loadedAssembliesByName.Add( typeof(Console).Assembly.GetName().Name!, typeof(Console).Assembly); } @@ -222,11 +211,17 @@ public static napi_value InitializeModule(napi_env env, napi_value exports) { JSObject exportsObject = (JSObject)new JSValue(exports, scope); - // Save the require() function that was passed in by the init script. - JSValue require = exportsObject["require"]; - if (require.IsFunction()) + // Save the require() and import() functions that were passed in by the init script. + JSValue requireFunction = exportsObject["require"]; + if (requireFunction.IsFunction()) + { + JSRuntimeContext.Current.RequireFunction = (JSFunction)requireFunction; + } + + JSValue importFunction = exportsObject["import"]; + if (importFunction.IsFunction()) { - JSRuntimeContext.Current.Require = require; + JSRuntimeContext.Current.ImportFunction = (JSFunction)importFunction; } ManagedHost host = new(exportsObject) @@ -513,7 +508,7 @@ private Assembly LoadAssembly(string assemblyNameOrFilePath, bool allowNativeLib assembly = _loadContext.LoadFromAssemblyPath(assemblyFilePath); #endif - _typeExporter.ExportAssemblyTypes(assembly, (JSObject)_exports.GetValue()!.Value); + _typeExporter.ExportAssemblyTypes(assembly); } catch (BadImageFormatException) { diff --git a/src/NodeApi.DotNetHost/TypeExporter.cs b/src/NodeApi.DotNetHost/TypeExporter.cs index f9228f50..78d0c75f 100644 --- a/src/NodeApi.DotNetHost/TypeExporter.cs +++ b/src/NodeApi.DotNetHost/TypeExporter.cs @@ -18,7 +18,19 @@ namespace Microsoft.JavaScript.NodeApi.DotNetHost; /// /// Dynamically exports .NET types to JS. /// -internal class TypeExporter +/// +/// Exporting a .NET type: +/// - Defines equivalent namespaced JS class prototype is defined, with a constructor function +/// that calls back to the.NET constructor. +/// - Defines static and instance properties and methods on the class prototype. Initially all +/// of them are stubs (if is true (the default), +/// but on first access each property gets redefined to call back to the corresponding .NET +/// property or method. The callback uses marshalling code dynamically generated by a +/// . +/// - Registers a mapping between the .NET type and JS class/constructor object with the +/// , for use in any marshalling operations. +/// +public class TypeExporter { /// /// Mapping from top-level namespace names like `System` and `Microsoft` to @@ -41,12 +53,24 @@ internal class TypeExporter /// private readonly JSMarshaller _marshaller; + private readonly JSReference? _namespaces; + /// /// Creates a new instance of the class. /// - public TypeExporter() + /// Used by the exporter to dynamically generate callback marshalling + /// code for exported members. Note the marshaller's + /// property controls the casing of members of exported types. + /// Optional JS object where top-level .NET namespace properties + /// (like "System") will be defined for exported types. + public TypeExporter(JSMarshaller marshaller, JSObject? namespaces = null) { - _marshaller = JSMarshaller.Current; + _marshaller = marshaller; + + if (namespaces != null) + { + _namespaces = new JSReference(namespaces.Value); + } } /// @@ -55,9 +79,22 @@ public TypeExporter() /// public bool IsDelayLoadEnabled { get; set; } = true; - public void ExportAssemblyTypes(Assembly assembly, JSObject exports) + /// + /// Exports all types from a .NET assembly to JavaScript. + /// + /// + /// If a JS "namespaces" object was passed to the constructor, + /// this method may register additional top-level namespaces on that object for types in the + /// assembly. + /// + /// If is true (the default), then individual types in the + /// assembly are not fully exported until they are referenced either directly or by a + /// dependency. + /// + public void ExportAssemblyTypes(Assembly assembly) { - Trace($"> ManagedHost.LoadAssemblyTypes({assembly.GetName().Name})"); + string assemblyName = assembly.GetName().Name!; + Trace($"> {nameof(TypeExporter)}.ExportAssemblyTypes({assemblyName})"); int count = 0; List typeProxies = new(); @@ -83,8 +120,14 @@ public void ExportAssemblyTypes(Assembly assembly, JSObject exports) { // Export a new top-level namespace. parentNamespace = new NamespaceProxy(namespaceParts[0], null, this); - exports[namespaceParts[0]] = parentNamespace.Value; _exportedNamespaces.Add(namespaceParts[0], parentNamespace); + + if (_namespaces != null) + { + // Add a property on the namespaces JS object. + JSObject namespacesObject = (JSObject)_namespaces.GetValue()!.Value; + namespacesObject[namespaceParts[0]] = parentNamespace.Value; + } } for (int i = 1; i < namespaceParts.Length; i++) @@ -147,7 +190,7 @@ public void ExportAssemblyTypes(Assembly assembly, JSObject exports) ExportExtensionMethod(extensionMethod); } - Trace($"< ManagedHost.LoadAssemblyTypes({assembly.GetName().Name}) => {count} types"); + Trace($"< {nameof(TypeExporter)}.ExportAssemblyTypes({assemblyName}) => {count} types"); } private void RegisterDerivedType(TypeProxy derivedType, Type? baseOrInterfaceType = null) @@ -265,7 +308,7 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten return true; } - public NamespaceProxy? GetNamespaceProxy(string ns) + internal NamespaceProxy? GetNamespaceProxy(string ns) { string[] namespaceParts = ns.Split('.'); if (!_exportedNamespaces.TryGetValue( @@ -287,7 +330,7 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten return namespaceProxy; } - public TypeProxy? GetTypeProxy(Type type) + internal TypeProxy? GetTypeProxy(Type type) { if (type.IsConstructedGenericType) { @@ -314,11 +357,11 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten /// never actually used. The default is from . /// A strong reference to a JS object that represents the exported type, or null /// if the type could not be exported. - public JSReference? TryExportType(Type type, bool? deferMembers = null) + internal JSReference? TryExportType(Type type, bool? deferMembers = null) { try { - return ExportType(type, deferMembers ?? IsDelayLoadEnabled); + return ExportType(type, deferMembers); } catch (NotSupportedException ex) { @@ -332,7 +375,24 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten } } - private JSReference ExportType(Type type, bool deferMembers) + /// + /// Exports a specific .NET type to JS. + /// + /// The .NET type to export. + /// True to delay exporting of all type members until each one is + /// accessed. If false, all type members are immediately exported, which may cascade to + /// exporting many additional types referenced by the members, including members that are + /// never actually used. The default is from . + /// A strong reference to a JS object that represents the exported type. + /// The .NET type cannot be exported. + /// + /// This method does NOT register namespaces for the exported type on the JS "namespaces" + /// object (if one was passed to the constructor). It is + /// sufficient for explicit marshalling of the exported type using .NET code, but not + /// for dynamic access of the .NET type from JS code. Use + /// instead for full namespace export. + /// + public JSReference ExportType(Type type, bool? deferMembers = null) { if (!IsSupportedType(type)) { @@ -358,7 +418,7 @@ private JSReference ExportType(Type type, bool deferMembers) } else { - return ExportClass(type, deferMembers); + return ExportClass(type, deferMembers ?? IsDelayLoadEnabled); } } else @@ -537,9 +597,15 @@ private void ExportTypeIfSupported(Type dependencyType, bool deferMembers) #endif IsSupportedType(dependencyType)) { - TypeProxy typeProxy = GetTypeProxy(dependencyType) ?? - throw new InvalidOperationException( - $"Type proxy not found for dependency: {dependencyType.FormatName()}"); + TypeProxy? typeProxy = GetTypeProxy(dependencyType); + if (typeProxy == null) + { + ExportAssemblyTypes(dependencyType.Assembly); + typeProxy = GetTypeProxy(dependencyType) ?? + throw new InvalidOperationException( + $"Dependency type was not exported: {dependencyType.FormatName()}"); + } + typeProxy.Export(); } } diff --git a/src/NodeApi/DotNetHost/NativeHost.cs b/src/NodeApi/DotNetHost/NativeHost.cs index eb0790df..8431e8f5 100644 --- a/src/NodeApi/DotNetHost/NativeHost.cs +++ b/src/NodeApi/DotNetHost/NativeHost.cs @@ -116,6 +116,7 @@ private JSValue InitializeManagedHost(JSCallbackArgs args) } JSValue require = args[2]; + JSValue import = args[3]; Trace($"> NativeHost.InitializeManagedHost({targetFramework}, {managedHostPath})"); try @@ -130,7 +131,8 @@ private JSValue InitializeManagedHost(JSCallbackArgs args) int.Parse(targetFramework.Substring(4, 1)), targetFramework.Length == 5 ? 0 : int.Parse(targetFramework.Substring(5, 1))); - exports = InitializeFrameworkHost(frameworkVersion, managedHostPath, require); + exports = InitializeFrameworkHost( + frameworkVersion, managedHostPath, require, import); } else { @@ -140,7 +142,8 @@ private JSValue InitializeManagedHost(JSCallbackArgs args) #else Version dotnetVersion = Version.Parse(targetFramework.AsSpan(3)); #endif - exports = InitializeDotNetHost(dotnetVersion, managedHostPath, require); + exports = InitializeDotNetHost( + dotnetVersion, managedHostPath, require, import); } // Save init parameters and result in case of re-init. @@ -166,11 +169,13 @@ private JSValue InitializeManagedHost(JSCallbackArgs args) /// Minimum requested .NET version. /// Path to the managed host assembly file. /// Require function passed in by the init script. + /// Import function passed in by the init script. /// JS exports value from the managed host. private JSValue InitializeFrameworkHost( Version minVersion, string managedHostPath, - JSValue require) + JSValue require, + JSValue import) { Trace(" Initializing .NET Framework " + minVersion); @@ -196,6 +201,7 @@ private JSValue InitializeFrameworkHost( // Create an "exports" object for the managed host module initialization. JSValue exportsValue = JSValue.CreateObject(); exportsValue.SetProperty("require", require); + exportsValue.SetProperty("import", import); napi_env env = (napi_env)exportsValue.Scope; napi_value exports = (napi_value)exportsValue; @@ -235,11 +241,13 @@ private JSValue InitializeFrameworkHost( /// Requested .NET version. /// Path to the managed host assembly file. /// Require function passed in by the init script. + /// Import function passed in by the init script. /// JS exports value from the managed host. private JSValue InitializeDotNetHost( Version targetVersion, string managedHostPath, - JSValue require) + JSValue require, + JSValue import) { Trace(" Initializing .NET " + targetVersion); @@ -304,6 +312,7 @@ private JSValue InitializeDotNetHost( // Create an "exports" object for the managed host module initialization. var exports = JSValue.CreateObject(); exports.SetProperty("require", require); + exports.SetProperty("import", import); // Define a dispose method implemented by the native host that closes the CLR context. // The managed host proxy will pass through dispose calls to this callback. diff --git a/src/NodeApi/Interop/EmptyAttributes.cs b/src/NodeApi/Interop/EmptyAttributes.cs index 0ebe9189..31eaaf14 100644 --- a/src/NodeApi/Interop/EmptyAttributes.cs +++ b/src/NodeApi/Interop/EmptyAttributes.cs @@ -65,4 +65,13 @@ public CallerArgumentExpressionAttribute(string parameterName) } } +namespace System.Diagnostics +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class StackTraceHiddenAttribute : Attribute + { + public StackTraceHiddenAttribute() {} + } +} + #endif diff --git a/src/NodeApi/Interop/JSRuntimeContext.cs b/src/NodeApi/Interop/JSRuntimeContext.cs index 92bf11dc..1e64f953 100644 --- a/src/NodeApi/Interop/JSRuntimeContext.cs +++ b/src/NodeApi/Interop/JSRuntimeContext.cs @@ -8,6 +8,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using Microsoft.JavaScript.NodeApi.Runtime; using static Microsoft.JavaScript.NodeApi.Interop.JSCollectionProxies; using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime; @@ -99,9 +100,14 @@ private readonly ConcurrentDictionary _objectMap private readonly ConcurrentDictionary<(string?, string?), JSReference> _importMap = new(); /// - /// Holds a reference to the require() function. + /// Holds a reference to the synchronous CommonJS require() function. /// - private JSReference? _require; + private JSReference? _requireFunction; + + /// + /// Holds a reference to the asynchronous ES import() function. + /// + private JSReference? _importFunction; private readonly ConcurrentDictionary _collectionProxyHandlerMap = new(); @@ -155,19 +161,19 @@ internal JSRuntimeContext( } /// - /// Gets or sets the require() function. + /// Gets or sets the require() function, that supports synchronously importing CommonJS modules. /// /// /// Managed-host initialization will typically pass in the require function. /// - public JSValue Require + public JSFunction RequireFunction { get { - JSValue? value = _require?.GetValue(); + JSValue? value = _requireFunction?.GetValue(); if (value?.IsFunction() == true) { - return value.Value; + return (JSFunction)value.Value; } JSValue globalObject = JSValue.Global[GlobalObjectName]; @@ -176,19 +182,57 @@ public JSValue Require JSValue globalRequire = globalObject["require"]; if (globalRequire.IsFunction()) { - _require = new JSReference(globalRequire); - return globalRequire; + _requireFunction = new JSReference(globalRequire); + return (JSFunction)globalRequire; } } throw new InvalidOperationException( $"The require function was not found on the global {GlobalObjectName} object. " + - $"Set `global.{GlobalObjectName} = {{ require }}` before loading the module."); + $"Set `global.{GlobalObjectName}.require` before loading the module."); } set { - _require?.Dispose(); - _require = new JSReference(value); + _requireFunction?.Dispose(); + _requireFunction = new JSReference(value); + } + } + + /// + /// Gets or sets the import() function, that supports asynchronously importing ES modules. + /// + /// + /// Managed-host initialization will typically pass in the import function. + /// + public JSFunction ImportFunction + { + get + { + JSValue? value = _importFunction?.GetValue(); + if (value?.IsFunction() == true) + { + return (JSFunction)value.Value; + } + + JSValue globalObject = JSValue.Global[GlobalObjectName]; + if (globalObject.IsObject()) + { + JSValue globalImport = globalObject["import"]; + if (globalImport.IsFunction()) + { + _importFunction = new JSReference(globalImport); + return (JSFunction)globalImport; + } + } + + throw new InvalidOperationException( + $"The import function was not found on the global {GlobalObjectName} object. " + + $"Set `global.{GlobalObjectName}.import` before loading the module."); + } + set + { + _importFunction?.Dispose(); + _importFunction = new JSReference(value); } } @@ -570,12 +614,18 @@ public JSValue CreateStruct() where T : struct /// require() in JavaScript. Required if is null. /// Name of a property on the module (or global), or null to import /// the module object. Required if is null. - /// The imported value. + /// True to import an ES module; false to import a CommonJS module + /// (default). + /// The imported value. When importing from an ES module, this is a JS promise + /// that resolves to the imported value. /// Both and /// are null. - /// The function was - /// not initialized. - public JSValue Import(string? module, string? property = null) + /// The or + /// property was not initialized. + public JSValue Import( + string? module, + string? property = null, + bool esModule = false) { if ((module == null || module == "global") && property == null) { @@ -592,23 +642,64 @@ public JSValue Import(string? module, string? property = null) } else if (property == null) { - // Importing from a module via require(). - JSValue moduleValue = Require.Call(thisArg: JSValue.Undefined, module); + // Importing from a module via require() or import(). + JSFunction requireOrImport = esModule ? ImportFunction : RequireFunction; + JSValue moduleValue = requireOrImport.CallAsStatic(module); return new JSReference(moduleValue); } else { // Getting a property on an imported module. JSValue moduleValue = Import(module, null); - JSValue propertyValue = moduleValue.IsUndefined() ? - JSValue.Undefined : moduleValue.GetProperty(property); - return new JSReference(propertyValue); + if (esModule) + { + return new JSReference(((JSPromise)moduleValue).Then( + (value) => value.IsUndefined() ? + JSValue.Undefined : value.GetProperty(property))); + } + else + { + JSValue propertyValue = moduleValue.IsUndefined() ? + JSValue.Undefined : moduleValue.GetProperty(property); + return new JSReference(propertyValue); + } } }); return reference.GetValue() ?? JSValue.Undefined; } + /// + /// Imports a module or module property from JavaScript. + /// + /// Name of the module being imported, or null to import a + /// global property. This is equivalent to the value provided to import or + /// require() in JavaScript. Required if is null. + /// Name of a property on the module (or global), or null to import + /// the module object. Required if is null. + /// True to import an ES module; false to import a CommonJS module + /// (default). + /// A task that results in the imported value. When importing from an ES module, + /// the task directly results in the imported value (not a JS promise). + /// Both and + /// are null. + /// The or + /// property was not initialized. + public async Task ImportAsync( + string? module, + string? property = null, + bool esModule = false) + { + JSValue value = Import(module, property, esModule); + + if (value.IsPromise()) + { + value = await ((JSPromise)value).AsTask(); + } + + return value; + } + public void Dispose() { if (IsDisposed) return; diff --git a/src/NodeApi/Interop/JSSynchronizationContext.cs b/src/NodeApi/Interop/JSSynchronizationContext.cs index 9c8f5551..8ef8f968 100644 --- a/src/NodeApi/Interop/JSSynchronizationContext.cs +++ b/src/NodeApi/Interop/JSSynchronizationContext.cs @@ -125,7 +125,7 @@ public void Run(Action action) } else { - Exception? exception = null; + JSException? exception = null; Send((_) => { if (IsDisposed) return; @@ -135,12 +135,12 @@ public void Run(Action action) } catch (Exception ex) { - exception = ex; + exception = new JSException(ex); } }, null); if (exception != null) { - throw new JSException("Exception thrown from JS thread.", exception); + throw exception; } } } @@ -161,7 +161,7 @@ public T Run(Func action) else { T result = default!; - Exception? exception = null; + JSException? exception = null; Send((_) => { if (IsDisposed) return; @@ -171,12 +171,12 @@ public T Run(Func action) } catch (Exception ex) { - exception = ex; + exception = new JSException(ex); } }, null); if (exception != null) { - throw new JSException("Exception thrown from JS thread.", exception); + throw exception; } return result; } @@ -205,7 +205,7 @@ public Task RunAsync(Func asyncAction) } catch (Exception ex) { - completion.TrySetException(ex); + completion.TrySetException(new JSException(ex)); } }, null); return completion.Task; @@ -235,7 +235,7 @@ public Task RunAsync(Func> asyncAction) } catch (Exception ex) { - completion.TrySetException(ex); + completion.TrySetException(new JSException(ex)); } }, null); return completion.Task; diff --git a/src/NodeApi/JSException.cs b/src/NodeApi/JSException.cs index 3666cf5f..4f99910a 100644 --- a/src/NodeApi/JSException.cs +++ b/src/NodeApi/JSException.cs @@ -11,6 +11,11 @@ namespace Microsoft.JavaScript.NodeApi; /// public class JSException : Exception { + /// + /// Captures the JS stack when an exception propagates outside of the JS thread. + /// + private string? _jsStack; + /// /// Creates a new instance of with an exception message /// and optional inner exception. @@ -29,6 +34,25 @@ public JSException(JSError error) : base(error.Message) Error = error; } + /// + /// Creates a new instance of specifically for propagating + /// an already-thrown JS exception out to another thread. + /// + /// Exception that was already thrown from the JS thread. + /// + /// This constructor must be called while still on the JS thread. + /// + internal JSException(Exception innerException) + : this("Exception thrown from JS thread. See inner exception for details.", innerException) + { + JSException? innerJSException = innerException as JSException; + JSValue? jsError = innerJSException?.Error?.Value; + if (jsError is not null) + { + innerJSException!._jsStack = (string)jsError.Value["stack"]; + } + } + /// /// Gets the JavaScript error that caused this exception, or null if the exception /// was not caused by a JavaScript error. @@ -42,41 +66,40 @@ public override string? StackTrace { get { - JSValue? jsError = Error?.Value; - if (jsError is not null) + string? jsStack = _jsStack; + if (jsStack is null) { - string jsStack = (string)jsError.Value["stack"]; - - // The first line of the stack is the error type name and message, - // which is redundant when merged with the .NET exception. - int firstLineEnd = jsStack.IndexOf('\n'); - if (firstLineEnd >= 0) + JSValue? jsError = Error?.Value; + if (jsError is not null) { - jsStack = jsStack.Substring(firstLineEnd + 1); + jsStack = _jsStack ?? (string)jsError.Value["stack"]; } + } - // Normalize indentation to 3 spaces, as used by .NET. - // (JS traces indent with 4 spaces.) - if (jsStack.StartsWith(" at ")) - { - jsStack = jsStack.Replace(" at ", " at "); - } + if (string.IsNullOrEmpty(jsStack)) + { + // There's no JS stack, so just return the normal .NET stack. + return base.StackTrace; + } - // Strip the ThrowIfFailed() line(s) from the .NET stack trace. - string dotnetStack = base.StackTrace?.TrimStart(s_trimChars) ?? - string.Empty; - firstLineEnd = dotnetStack.IndexOf('\n'); - while (firstLineEnd >= 0 && dotnetStack.IndexOf( - "." + nameof(NodeApiStatusExtensions.ThrowIfFailed), 0, firstLineEnd) >= 0) - { - dotnetStack = dotnetStack.Substring(firstLineEnd + 1); - firstLineEnd = dotnetStack.IndexOf('\n'); - } + // The first line of the JS stack is the error type name and message, + // which is redundant when merged with the .NET exception. + int firstLineEnd = jsStack!.IndexOf('\n'); + if (firstLineEnd >= 0) + { + jsStack = jsStack.Substring(firstLineEnd + 1); + } - return jsStack + "\n" + dotnetStack; + // Normalize indentation to 3 spaces, as used by .NET. + // (JS traces indent with 4 spaces.) + if (jsStack.StartsWith(" at ")) + { + jsStack = jsStack.Replace(" at ", " at "); } - return base.StackTrace; + string dotnetStack = base.StackTrace?.TrimStart(s_trimChars) ?? + string.Empty; + return jsStack + "\n" + dotnetStack; } } diff --git a/src/NodeApi/JSPromise.cs b/src/NodeApi/JSPromise.cs index 04fc3113..8e8b5f76 100644 --- a/src/NodeApi/JSPromise.cs +++ b/src/NodeApi/JSPromise.cs @@ -143,20 +143,14 @@ async void AsyncCallback() /// Registers callbacks that are invoked when a promise is fulfilled and/or rejected, /// and returns a new chained promise. /// - public JSPromise Then(Action? fulfilled, Action? rejected) + public JSPromise Then( + Func? fulfilled, + Func? rejected = null) { JSValue fulfilledFunction = fulfilled == null ? JSValue.Undefined : - JSValue.CreateFunction(nameof(fulfilled), (args) => - { - fulfilled(args[0]); - return JSValue.Undefined; - }); + JSValue.CreateFunction(nameof(fulfilled), (args) => fulfilled(args[0])); JSValue rejectedFunction = rejected == null ? JSValue.Undefined : - JSValue.CreateFunction(nameof(rejected), (args) => - { - rejected(new JSError(args[0])); - return JSValue.Undefined; - }); + JSValue.CreateFunction(nameof(rejected), (args) => rejected(new JSError(args[0]))); return (JSPromise)_value.CallMethod("then", fulfilledFunction, rejectedFunction); } @@ -164,13 +158,10 @@ public JSPromise Then(Action? fulfilled, Action? rejected) /// Registers a callback that is invoked when a promise is rejected, and returns a new /// chained promise. /// - public JSPromise Catch(Action rejected) + public JSPromise Catch(Func rejected) { - JSValue rejectedFunction = JSValue.CreateFunction(nameof(rejected), (args) => - { - rejected(args[0]); - return JSValue.Undefined; - }); + JSValue rejectedFunction = JSValue.CreateFunction( + nameof(rejected), (args) => rejected(new JSError(args[0]))); return (JSPromise)_value.CallMethod("catch", rejectedFunction); } diff --git a/src/NodeApi/JSPromiseExtensions.cs b/src/NodeApi/JSPromiseExtensions.cs index a388ba0b..3eb52901 100644 --- a/src/NodeApi/JSPromiseExtensions.cs +++ b/src/NodeApi/JSPromiseExtensions.cs @@ -14,10 +14,15 @@ public static Task AsTask(this JSPromise promise) { TaskCompletionSource completion = new(); promise.Then( - completion.SetResult, + (JSValue value) => + { + completion.SetResult(value); + return default; + }, (JSError error) => { completion.SetException(new JSException(error)); + return default; }); return completion.Task; } diff --git a/src/NodeApi/NodeApi.csproj b/src/NodeApi/NodeApi.csproj index 71526798..e1c15086 100644 --- a/src/NodeApi/NodeApi.csproj +++ b/src/NodeApi/NodeApi.csproj @@ -41,6 +41,13 @@ + + + + PreserveNewest + true + + diff --git a/src/NodeApi/NodeApiStatusExtensions.cs b/src/NodeApi/NodeApiStatusExtensions.cs index 18ac0d9d..62596899 100644 --- a/src/NodeApi/NodeApiStatusExtensions.cs +++ b/src/NodeApi/NodeApiStatusExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime; @@ -9,6 +10,7 @@ namespace Microsoft.JavaScript.NodeApi; public static class NodeApiStatusExtensions { + [StackTraceHidden] public static void FatalIfFailed([DoesNotReturnIf(true)] this napi_status status, string? message = null, [CallerMemberName] string memberName = "", @@ -28,6 +30,7 @@ public static void FatalIfFailed([DoesNotReturnIf(true)] this napi_status status JSError.Fatal(message!, memberName, sourceFilePath, sourceLineNumber); } + [StackTraceHidden] public static void ThrowIfFailed([DoesNotReturnIf(true)] this napi_status status, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", @@ -46,6 +49,7 @@ public static void ThrowIfFailed([DoesNotReturnIf(true)] this napi_status status // Throw if status is not napi_ok. Otherwise, return the provided value. // This function helps writing compact wrappers for the interop calls. + [StackTraceHidden] public static T ThrowIfFailed(this napi_status status, T value, [CallerMemberName] string memberName = "", diff --git a/src/NodeApi/Runtime/NodejsEnvironment.cs b/src/NodeApi/Runtime/NodejsEnvironment.cs index 46113d81..f7dd4701 100644 --- a/src/NodeApi/Runtime/NodejsEnvironment.cs +++ b/src/NodeApi/Runtime/NodejsEnvironment.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.JavaScript.NodeApi.Interop; @@ -37,7 +38,7 @@ public static explicit operator napi_env(NodejsEnvironment environment) => public static implicit operator JSValueScope(NodejsEnvironment environment) => environment._scope; - internal NodejsEnvironment(NodejsPlatform platform, string? mainScript) + internal NodejsEnvironment(NodejsPlatform platform, string? baseDir, string? mainScript) { JSValueScope scope = null!; JSSynchronizationContext syncContext = null!; @@ -56,8 +57,16 @@ internal NodejsEnvironment(NodejsPlatform platform, string? mainScript) scope = new JSValueScope(JSValueScopeType.Root, env, platform.Runtime); syncContext = scope.RuntimeContext.SynchronizationContext; - // The require() function is available as a global in this context. - scope.RuntimeContext.Require = JSValue.Global["require"]; + if (string.IsNullOrEmpty(baseDir)) + { + baseDir = "."; + } + else + { + JSValue.Global.SetProperty("__dirname", baseDir!); + } + + InitializeModuleImportFunctions(scope.RuntimeContext, baseDir!); loadedEvent.Set(); @@ -77,6 +86,81 @@ internal NodejsEnvironment(NodejsPlatform platform, string? mainScript) SynchronizationContext = syncContext; } + private static void InitializeModuleImportFunctions( + JSRuntimeContext runtimeContext, + string baseDir) + { + // The require function is available as a global in the embedding context. + JSFunction originalRequire = (JSFunction)JSValue.Global["require"]; + JSReference originalRequireRef = new(originalRequire); + JSFunction envRequire = new("require", (modulePath) => + { + JSValue require = originalRequireRef.GetValue()!.Value; + JSValue resolvedPath = ResolveModulePath(require, modulePath, baseDir); + return require.Call(thisArg: default, resolvedPath); + }); + + // Also set up a callback for require.resolve(), in case it is used by imported modules. + JSValue requireObject = (JSValue)envRequire; + requireObject["resolve"] = new JSFunction("resolve", (modulePath) => + { + JSValue require = originalRequireRef.GetValue()!.Value; + return ResolveModulePath(require, modulePath, baseDir); + }); + + JSValue.Global.SetProperty("require", envRequire); + runtimeContext.RequireFunction = envRequire; + + // The import keyword is not a function and is only available through use of an + // external helper module. +#pragma warning disable IL3000 // Assembly.Location returns an empty string for assemblies embedded in a single-file app + string assemblyLocation = typeof(NodejsEnvironment).Assembly.Location; +#pragma warning restore IL3000 + if (!string.IsNullOrEmpty(assemblyLocation)) + { + string importAdapterModulePath = Path.Combine( + Path.GetDirectoryName(assemblyLocation)!, "import.cjs"); + if (File.Exists(importAdapterModulePath)) + { + JSFunction originalImport = (JSFunction)originalRequire.CallAsStatic( + importAdapterModulePath); + JSReference originalImportRef = new(originalImport); + JSFunction envImport = new("import", (modulePath) => + { + JSValue require = originalRequireRef.GetValue()!.Value; + JSValue resolvedPath = ResolveModulePath(require, modulePath, baseDir); + JSValue moduleUri = "file://" + (string)resolvedPath; + JSValue import = originalImportRef.GetValue()!.Value; + return import.Call(thisArg: default, moduleUri); + }); + + JSValue.Global.SetProperty("import", envImport); + runtimeContext.ImportFunction = envImport; + } + } + } + + /// + /// Use the require.resolve() function with an explicit base directory to resolve both + /// CommonJS and ES modules. + /// + /// Require function. + /// Module name or path that was supplied to the require or import + /// function. + /// Base directory for the module resolution. + /// Resolved module path. + /// Thrown if the module could not be resolved. + private static JSValue ResolveModulePath( + JSValue require, + JSValue modulePath, + string baseDir) + { + // Pass the base directory to require.resolve() via the options object. + JSObject options = new(); + options["paths"] = new JSArray(new[] { (JSValue)baseDir! }); + return require.CallMethod("resolve", modulePath, options); + } + /// /// Gets a synchronization context that enables switching to the Node.js environment's thread /// and returning to the thread after an `await`. @@ -284,8 +368,11 @@ public Task RunAsync(Func> asyncAction) /// are null. /// The function was /// not initialized. - public JSValue Import(string? module, string? property = null) - => _scope.RuntimeContext.Import(module, property); + public JSValue Import(string? module, string? property = null, bool esModule = false) + => _scope.RuntimeContext.Import(module, property, esModule); + + public Task ImportAsync(string? module, string? property = null, bool esModule = false) + => _scope.RuntimeContext.ImportAsync(module, property, esModule); /// /// Runs garbage collection in the JS environment. diff --git a/src/NodeApi/Runtime/NodejsPlatform.cs b/src/NodeApi/Runtime/NodejsPlatform.cs index 8e49ad20..fe0c9b4a 100644 --- a/src/NodeApi/Runtime/NodejsPlatform.cs +++ b/src/NodeApi/Runtime/NodejsPlatform.cs @@ -76,13 +76,18 @@ public void Dispose() /// /// Creates a new Node.js environment with a dedicated main thread. /// + /// Optional directory that is used as the base directory when resolving + /// imported modules, and also as the value of the global `__dirname` property. If unspecified, + /// modules are resolved relative to the process CWD and `__dirname` is undefined. /// Optional script to run in the environment. (Literal script content, /// not a path to a script file.) /// A new instance. - public NodejsEnvironment CreateEnvironment(string? mainScript = null) + public NodejsEnvironment CreateEnvironment( + string? baseDir = null, + string? mainScript = null) { if (IsDisposed) throw new ObjectDisposedException(nameof(NodejsPlatform)); - return new NodejsEnvironment(this, mainScript); + return new NodejsEnvironment(this, baseDir, mainScript); } } diff --git a/src/NodeApi/import.cjs b/src/NodeApi/import.cjs new file mode 100644 index 00000000..0dc1fd53 --- /dev/null +++ b/src/NodeApi/import.cjs @@ -0,0 +1,3 @@ +// This module wraps the ES import keyword in a CommonJS function, +// to enable directly importing ES modules into .NET. +module.exports = function importModule(modulePath) { return import(modulePath); }; diff --git a/src/node-api-dotnet/init.js b/src/node-api-dotnet/init.js index b80e5fd1..c9695aaa 100644 --- a/src/node-api-dotnet/init.js +++ b/src/node-api-dotnet/init.js @@ -46,6 +46,11 @@ function initialize(targetFramework) { const managedHostPath = __dirname + `/${targetFramework}/${assemblyName}.DotNetHost.dll` const nativeHost = require(nativeHostPath); - dotnet = nativeHost.initialize(targetFramework, managedHostPath, require); + + // Pass require() and import() functions to the host initialize() method. + // Since `import` is a keyword and not a function it has to be wrapped in a function value. + const importModule = function importModule(modulePath) { return import(modulePath); }; + + dotnet = nativeHost.initialize(targetFramework, managedHostPath, require, importModule); return dotnet; } diff --git a/test/NodejsEmbeddingTests.cs b/test/NodejsEmbeddingTests.cs index 4df46c77..9e60268e 100644 --- a/test/NodejsEmbeddingTests.cs +++ b/test/NodejsEmbeddingTests.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using Microsoft.JavaScript.NodeApi.DotNetHost; using Microsoft.JavaScript.NodeApi.Runtime; using Xunit; using static Microsoft.JavaScript.NodeApi.Test.TestUtils; @@ -22,7 +24,7 @@ public class NodejsEmbeddingTests internal static NodejsEnvironment CreateNodejsEnvironment() { Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath); - return NodejsPlatform.CreateEnvironment(); + return NodejsPlatform.CreateEnvironment(Path.Combine(GetRepoRootDirectory(), "test")); } internal static void RunInNodejsEnvironment(Action action) @@ -32,11 +34,11 @@ internal static void RunInNodejsEnvironment(Action action) } [SkippableFact] - public void NodejsStart() + public void StartEnvironment() { using NodejsEnvironment nodejs = CreateNodejsEnvironment(); - nodejs.SynchronizationContext.Run(() => + nodejs.Run(() => { JSValue result = JSValue.RunScript("require('node:path').join('a', 'b')"); Assert.Equal(Path.Combine("a", "b"), (string)result); @@ -47,15 +49,17 @@ public void NodejsStart() } [SkippableFact] - public void NodejsRestart() + public void RestartEnvironment() { // Create and destory a Node.js environment twice, using the same platform instance. - NodejsStart(); - NodejsStart(); + StartEnvironment(); + StartEnvironment(); } + public interface IConsole { void Log(string message); } + [SkippableFact] - public void NodejsCallFunction() + public void CallFunction() { using NodejsEnvironment nodejs = CreateNodejsEnvironment(); @@ -70,7 +74,105 @@ public void NodejsCallFunction() } [SkippableFact] - public void NodejsUnhandledRejection() + public void ImportBuiltinModule() + { + using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + + nodejs.Run(() => + { + JSValue fsModule = nodejs.Import("fs"); + Assert.Equal(JSValueType.Object, fsModule.TypeOf()); + Assert.Equal(JSValueType.Function, fsModule["stat"].TypeOf()); + + JSValue nodeFsModule = nodejs.Import("node:fs"); + Assert.Equal(JSValueType.Object, nodeFsModule.TypeOf()); + Assert.Equal(JSValueType.Function, nodeFsModule["stat"].TypeOf()); + }); + + nodejs.Dispose(); + Assert.Equal(0, nodejs.ExitCode); + } + + [SkippableFact] + public void ImportCommonJSModule() + { + using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + + nodejs.Run(() => + { + JSValue testModule = nodejs.Import("./test-module.cjs"); + Assert.Equal(JSValueType.Object, testModule.TypeOf()); + Assert.Equal(JSValueType.Function, testModule["test"].TypeOf()); + Assert.Equal("test", testModule.CallMethod("test")); + }); + + nodejs.Dispose(); + Assert.Equal(0, nodejs.ExitCode); + } + + [SkippableFact] + public void ImportCommonJSPackage() + { + using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + + nodejs.Run(() => + { + JSValue testModule = nodejs.Import("./test-cjs-package"); + Assert.Equal(JSValueType.Object, testModule.TypeOf()); + Assert.Equal(JSValueType.Function, testModule["test"].TypeOf()); + Assert.Equal("test", testModule.CallMethod("test")); + }); + + nodejs.Dispose(); + Assert.Equal(0, nodejs.ExitCode); + } + + [SkippableFact] + public async Task ImportESModule() + { + using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + + await nodejs.RunAsync(async () => + { + JSValue testModule = await nodejs.ImportAsync( + "./test-module.mjs", null, esModule: true); + Assert.Equal(JSValueType.Object, testModule.TypeOf()); + Assert.Equal(JSValueType.Function, testModule["test"].TypeOf()); + Assert.Equal("test", testModule.CallMethod("test")); + }); + + nodejs.Dispose(); + Assert.Equal(0, nodejs.ExitCode); + } + + [SkippableFact] + public async Task ImportESPackage() + { + using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + + await nodejs.RunAsync(async () => + { + JSValue testModule = await nodejs.ImportAsync( + "./test-esm-package", null, esModule: true); + Assert.Equal(JSValueType.Object, testModule.TypeOf()); + Assert.Equal(JSValueType.Function, testModule["test"].TypeOf()); + Assert.Equal("test", testModule.CallMethod("test")); + + // Check that module resolution handles sub-paths from conditional exports. + // https://nodejs.org/api/packages.html#conditional-exports + JSValue testModuleFeature = await nodejs.ImportAsync( + "./test-esm-package/feature", null, esModule: true); + Assert.Equal(JSValueType.Object, testModuleFeature.TypeOf()); + Assert.Equal(JSValueType.Function, testModuleFeature["test2"].TypeOf()); + Assert.Equal("test2", testModuleFeature.CallMethod("test2")); + }); + + nodejs.Dispose(); + Assert.Equal(0, nodejs.ExitCode); + } + + [SkippableFact] + public void UnhandledRejection() { using NodejsEnvironment nodejs = CreateNodejsEnvironment(); @@ -80,7 +182,7 @@ public void NodejsUnhandledRejection() errorMessage = (string)e.Error.GetProperty("message"); }; - nodejs.SynchronizationContext.Run(() => + nodejs.Run(() => { JSValue.RunScript("new Promise((resolve, reject) => reject(new Error('test')))"); }); @@ -92,32 +194,31 @@ public void NodejsUnhandledRejection() } [SkippableFact] - public void NodejsErrorPropagation() + public void ErrorPropagation() { using NodejsEnvironment nodejs = CreateNodejsEnvironment(); - string? exceptionMessage = null; - string? exceptionStack = null; - - nodejs.SynchronizationContext.Run(() => + JSException exception = Assert.Throws(() => { - try + nodejs.Run(() => { JSValue.RunScript( "function throwError() { throw new Error('test'); }\n" + "throwError();"); - } - catch (JSException ex) - { - exceptionMessage = ex.Message; - exceptionStack = ex.StackTrace; - } + }); }); - Assert.Equal("test", exceptionMessage); + Assert.StartsWith("Exception thrown from JS thread.", exception.Message); + Assert.IsType(exception.InnerException); + + exception = (JSException)exception.InnerException; + Assert.Equal("test", exception.Message); - Assert.NotNull(exceptionStack); - string[] stackLines = exceptionStack.Split('\n').Select((line) => line.Trim()).ToArray(); + Assert.NotNull(exception.StackTrace); + string[] stackLines = exception.StackTrace + .Split('\n') + .Select((line) => line.Trim()) + .ToArray(); // The first line of the stack trace should refer to the JS function that threw. Assert.StartsWith("at throwError ", stackLines[0]); @@ -127,4 +228,34 @@ public void NodejsErrorPropagation() stackLines, (line) => line.StartsWith($"at {typeof(NodejsEmbeddingTests).FullName}.")); } + + /// + /// Tests the functionality of dynamically exporting and marshalling a class type from .NET + /// to JS (as opposed to relying on [JSExport] (compile-time code-generation) for marshalling. + /// + [SkippableFact] + public void MarshalClass() + { + using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + + nodejs.Run(() => + { + JSMarshaller marshaller = new(); + TypeExporter exporter = new(marshaller); + exporter.ExportType(typeof(TestClass)); + TestClass obj = new() + { + Value = "test" + }; + JSValue objJs = marshaller.ToJS(obj); + Assert.Equal(JSValueType.Object, objJs.TypeOf()); + Assert.Equal("test", (string)objJs["Value"]); + }); + } + + // Used for marshalling tests. + public class TestClass + { + public string? Value { get; set; } + } } diff --git a/test/test-cjs-package/index.js b/test/test-cjs-package/index.js new file mode 100644 index 00000000..da801d34 --- /dev/null +++ b/test/test-cjs-package/index.js @@ -0,0 +1 @@ +exports.test = function test() { return 'test'; } diff --git a/test/test-cjs-package/package.json b/test/test-cjs-package/package.json new file mode 100644 index 00000000..5af91cfb --- /dev/null +++ b/test/test-cjs-package/package.json @@ -0,0 +1,5 @@ +{ + "name": "node-api-dotnet-test-cjs-package", + "type": "commonjs", + "main": "index.js" +} diff --git a/test/test-esm-package/feature.js b/test/test-esm-package/feature.js new file mode 100644 index 00000000..1db98656 --- /dev/null +++ b/test/test-esm-package/feature.js @@ -0,0 +1 @@ +export function test2() { return 'test2'; } diff --git a/test/test-esm-package/index.js b/test/test-esm-package/index.js new file mode 100644 index 00000000..5adcadbb --- /dev/null +++ b/test/test-esm-package/index.js @@ -0,0 +1 @@ +export function test() { return 'test'; } diff --git a/test/test-esm-package/package.json b/test/test-esm-package/package.json new file mode 100644 index 00000000..6ec6bbcc --- /dev/null +++ b/test/test-esm-package/package.json @@ -0,0 +1,8 @@ +{ + "name": "node-api-dotnet-test-cjs-package", + "type": "module", + "exports": { + ".": "./index.js", + "./feature": "./feature.js" + } +} diff --git a/test/test-module.cjs b/test/test-module.cjs new file mode 100644 index 00000000..da801d34 --- /dev/null +++ b/test/test-module.cjs @@ -0,0 +1 @@ +exports.test = function test() { return 'test'; } diff --git a/test/test-module.mjs b/test/test-module.mjs new file mode 100644 index 00000000..5adcadbb --- /dev/null +++ b/test/test-module.mjs @@ -0,0 +1 @@ +export function test() { return 'test'; } diff --git a/version.json b/version.json index 82ef8bde..885a3f51 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.5", + "version": "0.6", "publicReleaseRefSpec": [ "^refs/heads/main$",