From 72f1cd10517510478f110e9a87560d8ebbd96dad Mon Sep 17 00:00:00 2001 From: Jason Ginchereau Date: Thu, 11 Jul 2024 09:27:00 -1000 Subject: [PATCH] New docs site (#318) --- .github/workflows/docs.yml | 75 + .gitignore | 3 + Directory.Build.props | 4 +- Directory.Packages.props | 1 + README.md | 8 +- docs/.gitignore | 6 + docs/.vitepress/config.mts | 122 ++ docs/.vitepress/theme/custom.css | 10 + docs/.vitepress/theme/index.ts | 4 + {Docs => docs}/DotnetToJS.drawio.svg | 0 {Docs => docs}/JStoDotnet.drawio.svg | 0 {Docs => docs}/NodeApi-Layers.drawio.svg | 0 {Docs => docs}/NodeApi-Layers.md | 0 docs/contributing.md | 9 + {Docs => docs}/dates.md | 0 {Docs => docs}/dynamic-invoke.md | 7 +- docs/examples.md | 4 + docs/features/dotnet-native-aot.md | 44 + docs/features/js-dotnet-marshalling.md | 120 ++ docs/features/js-references.md | 66 + docs/features/js-threading-async.md | 72 + docs/features/js-types-in-dotnet.md | 88 + docs/features/js-value-scopes.md | 74 + docs/features/performance.md | 20 + docs/features/type-definitions.md | 57 + {Docs => docs}/generics.md | 0 docs/images/dark/ts.svg | 10 + .../images/dotnet-bot_scene_coffee-shop.png | Bin docs/images/light/ts.svg | 10 + docs/images/node-api-dotnet-logo.svg | 265 +++ docs/index.md | 65 + {Docs => docs}/masrshalling-null-undefined.md | 0 {Docs => docs}/node-module.md | 11 +- docs/overview.md | 37 + docs/package-lock.json | 1776 +++++++++++++++++ docs/package.json | 14 + {Docs => docs}/presentation.html | 0 {Docs => docs}/presentation.md | 0 {Docs => docs}/presentation2.html | 0 {Docs => docs}/presentation2.md | 0 docs/reference/arrays-collections.md | 83 + docs/reference/async-promises.md | 42 + docs/reference/basic-types.md | 91 + docs/reference/classes-interfaces.md | 117 ++ docs/reference/dates.md | 66 + docs/reference/delegates.md | 27 + docs/reference/enums.md | 21 + docs/reference/events.md | 7 + docs/reference/exceptions.md | 40 + docs/reference/extension-methods.md | 31 + docs/reference/generics.md | 62 + docs/reference/js-apis.md | 49 + docs/reference/js-dotnet-types.md | 30 + docs/reference/msbuild-props.md | 15 + docs/reference/namespaces.md | 41 + docs/reference/null-undefined.md | 218 ++ docs/reference/other-types.md | 24 + docs/reference/overloaded-methods.md | 29 + docs/reference/packages-releases.md | 33 + docs/reference/ref-out-params.md | 90 + docs/reference/streams.md | 32 + docs/reference/structs-tuples.md | 48 + docs/requirements.md | 26 + docs/scenarios/dotnet-js.md | 94 + docs/scenarios/index.md | 36 + docs/scenarios/js-aot-module.md | 46 + docs/scenarios/js-dotnet-dynamic.md | 124 ++ docs/scenarios/js-dotnet-module.md | 103 + docs/support.md | 8 + docs/tools/XmlDocMarkdown.cs | 2 + docs/tools/XmlDocMarkdown.csproj | 8 + docs/tools/build-dotnet-api-docs.js | 301 +++ docs/tools/build-js-api-docs.js | 162 ++ {Docs => docs}/typescript.md | 0 src/NodeApi/DotNetHost/MSCorEE.cs | 2 +- src/NodeApi/DotNetHost/NativeHost.cs | 2 +- src/NodeApi/Interop/EmptyAttributes.cs | 10 - src/NodeApi/Interop/JSCallbackOverload.cs | 6 +- src/NodeApi/Interop/JSClassBuilderOfT.cs | 2 +- src/NodeApi/Interop/JSModuleAttribute.cs | 2 +- src/NodeApi/Interop/JSRuntimeContext.cs | 14 +- .../Interop/JSSynchronizationContext.cs | 5 +- src/NodeApi/Interop/JSThreadSafeFunction.cs | 6 + src/NodeApi/Interop/NodeStream.Proxy.cs | 2 +- src/NodeApi/JSCallback.cs | 11 + src/NodeApi/JSCallbackArgs.cs | 23 + src/NodeApi/JSDispatcherQueue.cs | 24 +- src/NodeApi/JSReference.cs | 12 + src/NodeApi/JSValue.cs | 24 + src/NodeApi/JSValueScope.cs | 6 +- src/NodeApi/JSValueScopeClosedException.cs | 2 +- src/NodeApi/Runtime/JSRuntime.Types.cs | 11 +- src/NodeApi/Runtime/JSRuntime.cs | 4 +- src/NodeApi/Runtime/NodejsEnvironment.cs | 13 +- src/NodeApi/Runtime/NodejsRuntime.Types.cs | 93 +- src/NodeApi/Runtime/TracingJSRuntime.cs | 40 +- src/node-api-dotnet/generator/index.js | 2 +- src/node-api-dotnet/index.d.ts | 41 +- src/node-api-dotnet/tsconfig.json | 5 + 99 files changed, 5328 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/.gitignore create mode 100644 docs/.vitepress/config.mts create mode 100644 docs/.vitepress/theme/custom.css create mode 100644 docs/.vitepress/theme/index.ts rename {Docs => docs}/DotnetToJS.drawio.svg (100%) rename {Docs => docs}/JStoDotnet.drawio.svg (100%) rename {Docs => docs}/NodeApi-Layers.drawio.svg (100%) rename {Docs => docs}/NodeApi-Layers.md (100%) create mode 100644 docs/contributing.md rename {Docs => docs}/dates.md (100%) rename {Docs => docs}/dynamic-invoke.md (95%) create mode 100644 docs/examples.md create mode 100644 docs/features/dotnet-native-aot.md create mode 100644 docs/features/js-dotnet-marshalling.md create mode 100644 docs/features/js-references.md create mode 100644 docs/features/js-threading-async.md create mode 100644 docs/features/js-types-in-dotnet.md create mode 100644 docs/features/js-value-scopes.md create mode 100644 docs/features/performance.md create mode 100644 docs/features/type-definitions.md rename {Docs => docs}/generics.md (100%) create mode 100644 docs/images/dark/ts.svg rename {Docs => docs}/images/dotnet-bot_scene_coffee-shop.png (100%) create mode 100644 docs/images/light/ts.svg create mode 100644 docs/images/node-api-dotnet-logo.svg create mode 100644 docs/index.md rename {Docs => docs}/masrshalling-null-undefined.md (100%) rename {Docs => docs}/node-module.md (92%) create mode 100644 docs/overview.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json rename {Docs => docs}/presentation.html (100%) rename {Docs => docs}/presentation.md (100%) rename {Docs => docs}/presentation2.html (100%) rename {Docs => docs}/presentation2.md (100%) create mode 100644 docs/reference/arrays-collections.md create mode 100644 docs/reference/async-promises.md create mode 100644 docs/reference/basic-types.md create mode 100644 docs/reference/classes-interfaces.md create mode 100644 docs/reference/dates.md create mode 100644 docs/reference/delegates.md create mode 100644 docs/reference/enums.md create mode 100644 docs/reference/events.md create mode 100644 docs/reference/exceptions.md create mode 100644 docs/reference/extension-methods.md create mode 100644 docs/reference/generics.md create mode 100644 docs/reference/js-apis.md create mode 100644 docs/reference/js-dotnet-types.md create mode 100644 docs/reference/msbuild-props.md create mode 100644 docs/reference/namespaces.md create mode 100644 docs/reference/null-undefined.md create mode 100644 docs/reference/other-types.md create mode 100644 docs/reference/overloaded-methods.md create mode 100644 docs/reference/packages-releases.md create mode 100644 docs/reference/ref-out-params.md create mode 100644 docs/reference/streams.md create mode 100644 docs/reference/structs-tuples.md create mode 100644 docs/requirements.md create mode 100644 docs/scenarios/dotnet-js.md create mode 100644 docs/scenarios/index.md create mode 100644 docs/scenarios/js-aot-module.md create mode 100644 docs/scenarios/js-dotnet-dynamic.md create mode 100644 docs/scenarios/js-dotnet-module.md create mode 100644 docs/support.md create mode 100644 docs/tools/XmlDocMarkdown.cs create mode 100644 docs/tools/XmlDocMarkdown.csproj create mode 100644 docs/tools/build-dotnet-api-docs.js create mode 100644 docs/tools/build-js-api-docs.js rename {Docs => docs}/typescript.md (100%) create mode 100644 src/node-api-dotnet/tsconfig.json diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..99def0d9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,75 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# Reference: https://vitepress.dev/guide/deploy#github-pages + +name: Deploy VitePress site to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +defaults: + run: + # All jobs in this workflow run from the docs directory + working-directory: docs + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install npm packages + run: npm ci + - name: Build JS API docs + run: npm run build-js + - name: Build .NET API docs + run: npm run build-dotnet + - name: Render docs with VitePress + run: npm run build + - name: Copy images + run: cp -r images .vitepress/dist/images + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: .vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7ac9bcb9..78967e96 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ examples/**/package-lock.json *.log *.binlog + +cache/ +dist/ diff --git a/Directory.Build.props b/Directory.Build.props index afc71af3..8678fb30 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -61,10 +61,12 @@ - $(DefineConstants);UNMANAGED_DELEGATES $(DefineConstants);STRING_AS_SPAN $(DefineConstants);STREAM_MEMORY $(DefineConstants);READONLY_SET + + + $(DefineConstants);UNMANAGED_DELEGATES diff --git a/Directory.Packages.props b/Directory.Packages.props index 6c22ddac..df70c5ff 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,5 +16,6 @@ + diff --git a/README.md b/README.md index 5dc75d58..5f19da5d 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ dotnet.ExampleNamespace.ExampleClass.ExampleMethod(...args); // This call is typ (CommonJS modules must use `require()` instead of `import`.) -For reference, there is a [list of C# type projections to TypeScript](/Docs/typescript.md). +For reference, there is a [list of C# type projections to TypeScript](/docs/typescript.md). ### Full async support JavaScript code can `await` a call to a .NET method that returns a `Task`. The marshaller @@ -282,12 +282,12 @@ For details, see [Using .NET Generics in JavaScript](./docs/generics.md). #### Instructions For calling .NET from JS, choose between one of the following scenarios: - - [Dynamically invoke .NET APIs from JavaScript](./Docs/dynamic-invoke.md)
+ - [Dynamically invoke .NET APIs from JavaScript](./docs/dynamic-invoke.md)
Dynamic invocation is simpler to set up: all you need is the `node-api-dotnet` npm package and the path to a .NET assembly you want to call. But it has some limitations (not all kinds of APIs are supported), and is not quite as fast as a C# module, because marshalling code must be generated at runtime. - - [Develop a Node module in C#](./Docs/node-module.md)
+ - [Develop a Node module in C#](./docs/node-module.md)
A C# Node module is appropriate for an application that has more advanced interop needs. It can be faster because marshalling code can be generated at compile time, and the shape of the APIs exposed to JavaScript can be adapted with JS interop in mind. @@ -330,4 +330,4 @@ third-party's policies.

-![.NET + JS scene](./Docs/images/dotnet-bot_scene_coffee-shop.png) +![.NET + JS scene](./docs/images/dotnet-bot_scene_coffee-shop.png) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..50a6ebb4 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,6 @@ +# API doc files are generated by the XmlDocMarkdown tool. +/reference/dotnet +/reference/js + +# Temporary files generated by vitepress +/.vitepress/.temp diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..2a33c264 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,122 @@ +import { defineConfig } from 'vitepress' +import dotnetApiNavTree from '../reference/dotnet/nav.mjs' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Node API for .NET", + description: "Advanced interoperability between .NET and JavaScript in the same process", + lang: 'en-US', + base: '/node-api-dotnet/', + + metaChunk: true, + + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + + logo: '/images/node-api-dotnet-logo.svg', + + sidebar: [ + { text: 'Overview', link: '/overview' }, + { + text: 'Get Started', + items: [ + { + text: 'JS / .NET interop scenarios', + link: '/scenarios/index', + collapsed: false, + items: [ + { text: 'Dynamic .NET from JS', link: '/scenarios/js-dotnet-dynamic' }, + { text: '.NET module for Node.js', link: '/scenarios/js-dotnet-module' }, + { text: '.NET Native AOT for Node.js', link: '/scenarios/js-aot-module' }, + { text: 'Embedding JS in .NET', link: '/scenarios/dotnet-js' }, + ], + }, + { 'text': 'Requirements', link: '/requirements' }, + { 'text': 'Example projects', link: '/examples' }, + ] + }, + { + text: 'Features', + items: [ + { text: 'Type definitions', link: '/features/type-definitions' }, + { text: 'JS / .NET Marshalling', link: '/features/js-dotnet-marshalling' }, + { text: 'JS types in .NET', link: '/features/js-types-in-dotnet' }, + { text: 'JS value scopes', link: '/features/js-value-scopes' }, + { text: 'JS threading & async', link: '/features/js-threading-async' }, + { text: 'JS references', link: '/features/js-references' }, + { text: '.NET Native AOT', link: '/features/dotnet-native-aot' }, + { text: 'Performance', link: '/features/performance' }, + ] + }, + { + text: 'Reference', + items: [ + { + text: 'JS / .NET type mappings', + link: '/reference/js-dotnet-types', + collapsed: true, + items: [ + { text: 'Basic types', link: '/reference/basic-types' }, + { text: 'Null & undefined', link: '/reference/null-undefined' }, + { text: 'Classes & interfaces', link: '/reference/classes-interfaces' }, + { text: 'Structs & tuples', link: '/reference/structs-tuples' }, + { text: 'Enums', link: '/reference/enums' }, + { text: 'Arrays & collections', link: '/reference/arrays-collections' }, + { text: 'Delegates', link: '/reference/delegates' }, + { text: 'Streams', link: '/reference/streams' }, + { text: 'Dates & times', link: '/reference/dates' }, + { text: 'Other special types', link: '/reference/other-types' }, + { text: 'Async & promises', link: '/reference/async-promises' }, + { text: 'Ref & out parameters', link: '/reference/ref-out-params' }, + { text: 'Generics', link: '/reference/generics' }, + { text: 'Extension methods', link: '/reference/extension-methods' }, + { text: 'Overloaded methods', link: '/reference/overloaded-methods' }, + { text: 'Events', link: '/reference/events' }, + { text: 'Exceptions', link: '/reference/exceptions' }, + { text: 'Namespaces', link: '/reference/namespaces' }, + ], + }, + { text: 'MSBuild properties', link: '/reference/msbuild-props' }, + { text: 'Packages & releases', link: '/reference/packages-releases' }, + ] + }, + + // API docs might belong under "Reference", but the vitepress sidebar has a max depth of 6, + // which .NET API docs would exceed if they were one level deeper. + { + text: 'JavaScript APIs', + items: [ + { + text: 'node-api-dotnet', + link: '/reference/js/', + } + ] + }, + { + text: '.NET APIs', + link: '/reference/dotnet/', + items: dotnetApiNavTree, + }, + + { text: 'Support', link: '/support' }, + { text: 'Contributing', link: '/contributing' }, + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/microsoft/node-api-dotnet' }, + ], + + search: { + provider: 'local', + }, + + editLink: { + pattern: 'https://github.com/microsoft/node-api-dotnet/docs/:path', + }, + + footer: { + message: 'Released under the MIT license', + copyright: 'Copyright © 2023-present Microsoft', + } + } +}) diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 00000000..6bf17220 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,10 @@ +/* + * This file has customizations for the VitePress default theme. For a list of + * variable defaults that can be overridden, see + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +:root { + /* The default sidebar width is 272px. We need extra width for a deep API doc tree. */ + --vp-sidebar-width: 400px; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 00000000..42fe9a93 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,4 @@ +import DefaultTheme from 'vitepress/theme' +import './custom.css' + +export default DefaultTheme diff --git a/Docs/DotnetToJS.drawio.svg b/docs/DotnetToJS.drawio.svg similarity index 100% rename from Docs/DotnetToJS.drawio.svg rename to docs/DotnetToJS.drawio.svg diff --git a/Docs/JStoDotnet.drawio.svg b/docs/JStoDotnet.drawio.svg similarity index 100% rename from Docs/JStoDotnet.drawio.svg rename to docs/JStoDotnet.drawio.svg diff --git a/Docs/NodeApi-Layers.drawio.svg b/docs/NodeApi-Layers.drawio.svg similarity index 100% rename from Docs/NodeApi-Layers.drawio.svg rename to docs/NodeApi-Layers.drawio.svg diff --git a/Docs/NodeApi-Layers.md b/docs/NodeApi-Layers.md similarity index 100% rename from Docs/NodeApi-Layers.md rename to docs/NodeApi-Layers.md diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..1c1a33ea --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,9 @@ +--- +prev: false +next: false +--- + +# Contributing + +https://github.com/microsoft/node-api-dotnet?tab=readme-ov-file#contributing +https://github.com/microsoft/node-api-dotnet/blob/main/README-DEV.md diff --git a/Docs/dates.md b/docs/dates.md similarity index 100% rename from Docs/dates.md rename to docs/dates.md diff --git a/Docs/dynamic-invoke.md b/docs/dynamic-invoke.md similarity index 95% rename from Docs/dynamic-invoke.md rename to docs/dynamic-invoke.md index 5ac69d01..547ef42b 100644 --- a/Docs/dynamic-invoke.md +++ b/docs/dynamic-invoke.md @@ -1,8 +1,9 @@ ## Dynamically invoke .NET APIs from JavaScript -For examples of this scenario, see -[../examples/dynamic-invoke/](../examples/dynamic-invoke/) or -[../examples/semantic-kernel/](../examples//semantic-kernel/). +For examples of this scenario, see the +[dotnet-dynamic](https://github.com/microsoft/node-api-dotnet/tree/main/examples/dotnet-dynamic) and +[semantic-kernel/](https://github.com/microsoft/node-api-dotnet/tree/main/examples/semantic-kernel) +projects. 1. (Optional but recommended) Create a `.csproj` project (without any `.cs` source files) that will manage restoring nuget packages for .NET assemblies used by JS: diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 00000000..7e0bee78 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,4 @@ +# Example Projects + +See the [examples](https://github.com/microsoft/node-api-dotnet/tree/main/examples) directory in +the repo. diff --git a/docs/features/dotnet-native-aot.md b/docs/features/dotnet-native-aot.md new file mode 100644 index 00000000..7d6bb488 --- /dev/null +++ b/docs/features/dotnet-native-aot.md @@ -0,0 +1,44 @@ +# .NET Native AOT + +This project supports loading .NET libraries into a JavaScript application process, +or loading JavaScript libraries into a .NET application process. In either case the .NET +code can be +[ahead-of-time (AOT) compiled](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/), +which makes it executable _without depending on the .NET Runtime_. + +There are advantages and disadvantages to either approach: +| | .NET Runtime | .NET Native AOT | +|---------------------|--------------|-----------------| +| API compatibility | Broad compatibility with .NET APIs | Limited compatibility with APIs designed to support AOT | +| Ease of deployment | Requires a matching version of .NET to be installed on the target system | A .NET installation is not required (though some platform libs may be required on Linux) +| Size of deployment | Compact - only IL assemblies need to be deployed | Larger due to bundling necessary runtime code - minimum ~3 MB per platform | +| Performance | Slightly slower startup (JIT) | Slightly faster startup (no JIT) | +| Runtime limitations | Full .NET functionality | Some .NET features like reflection and code-generation aren't supported | + +To use C# to create a Node.js addon using Native AOT, see +[.NET Native AOT for Node.js](../scenarios/js-aot-module). + +There is no documentation or example code yet specific to hosting JavaScript in a .NET Native AOT +application, but it is not very different from non-AOT +[Embedding JS in .NET](../scenarios/dotnet-js). + +## AOT limitations + +Some features in this project are not available in a Native AOT environment because they depend +on runtime reflection or code-generation: + - [Dynamically loading and invoking .NET APIs](../scenarios/js-dotnet-dynamic) - Only APIs tagged + with `[JSExport]` and + [compiled with the source-generator](./js-dotnet-marshalling#compile-time-code-generation) can + be called in an AOT module. + - [.NET namespaces](../reference/namespaces) - Namespaces are used only with dynamic invocation. + APIs exported by an AOT module do not use JS namespaces. + - [Constructing generic classes or invoking generic methods](../reference/generics) - AOT modules + can only export non-generic types and methods. + ([Generic collections](../reference/arrays-collections) are supported though.) + - [Calling .NET extension methods using extension syntax](../reference/extension-methods). + Extension methods can still be called using static-method syntax, but AOT modules should design + exported APIs to not require extension methods. + - Implementing a .NET interface with a JavaScript class - this requires code-generation to [emit + a .NET class that implements the interface as a proxy to the JS object]( + https://github.com/microsoft/node-api-dotnet/blob/main/src/NodeApi.DotNetHost/JSInterfaceMarshaller.cs). + AOT modules should not export APIs that expect an interface to be implemented by JS. diff --git a/docs/features/js-dotnet-marshalling.md b/docs/features/js-dotnet-marshalling.md new file mode 100644 index 00000000..66439def --- /dev/null +++ b/docs/features/js-dotnet-marshalling.md @@ -0,0 +1,120 @@ +# Marshalling between .NET and JavaScript + +In this project, the term "marshalling" refers the process of passing complex values between .NET +and JavaScript runtimes while applying necessary conversions and adapters. The marshaller component +of `node-api-dotnet` enables JavaScript code to call .NET APIs, pass complex parameters and receive +results, implement .NET interfaces, handle exceptions, work with shared memory, all in a manner +that is strongly-typed and (mostly) natural. Marshalling is bi-directional, so it also supports +.NET code calling JS APIs, and callbacks (delegates) in either direction. + +This page describes how the marshaller works generally. For details about how specific types and +language constructs are handled by the marshaller, refer to +[JavaScript / .NET Type Mappings](../reference/js-dotnet-types.md). + +## Adapting JavaScript calls to .NET + +At a lower level, all calls from JS come into .NET as invocations of the +[`JSCallback`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSCallback) delegate: +```C# +delegate JSValue JSCallback(JSCallbackArgs args); +``` + +An important job of the marshaller is to adapt that low-level callback to an invocation of a .NET +method with specific parameter and return types. For a typical method call, this involves the +following steps: + +1. Get the .NET object for the `this` argument. For a static method call (or constructor) the +`this` value is ignored. But for an instance method it is a JS object that "wraps" a .NET +object (see [Object lifetimes](#object-lifetimes) below), so the .NET object is obtained by calling +[`JSValue.Unwrap()`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValue/Unwrap) on the +`this` value. +2. Marshal each of the parameters (of type +[`JSValue`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValue)) to their .NET equivalents. +This may involve use of conversions and adapters, managing object lifetimes, and other details +[depending on the types being marshalled](../reference/js-dotnet-types). +3. If the method is overloaded, [resolve the correct overload](../reference/overloaded-methods). +4. Invoke the .NET method. +5. Marshal the .NET return value (if not void) back to a `JSValue`. +6. Handle any .NET exception and re-throw as JS `Error` with combined stack trace. +See [Exceptions](../reference/exceptions). + +It could be possible to reflect on the .NET method to be invoked to discover parameter and return +type info, and use that type info from reflection to drive the marshalling logic on every +invocation. But .NET reflection is inefficient, and not supported by .NET Native AOT. So for better +performance and native AOT compability, the JS marshaller relies on code-generation to minimize +reflection. + +## Marshalling code generation + +The [`JSMarshaller`](../reference/dotnet/Microsoft.JavaScript.NodeApi.DotNetHost/JSMarshaller) +class is responsible for generating code for converting values between JS and .NET environments. +Code generation is achieved by building +[expression trees](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/) +for converting basic types, and then combining the expression trees to convert more complex types. + +While expression trees have some limitations in that they cannot represent some more advanced C# +language features, they are sufficiently expressive to support the requirements of marshalling JS +values to and from .NET, which mostly inovlves straightforward calls to constructors, properties, +and methods. Generics are more complex to work with but still supported by expression trees. + +### Runtime code generation + +The `JSMarshaller` uses .NET reflection on classes, methods, parameters, etc. to generate +marshalling expressions, then dynamically +[compiles the expressions to delegates](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/expression-trees-execution). +Once compiled, the delegates are cached in memory so repeated invocations no longer require +reflection or code-generation. The compiled delegates are registered as Node API JS callbacks for a +[`JSFunction`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSFunction) or +[`JSClassBuilder`](../reference/dotnet/Microsoft.JavaScript.NodeApi.Interop/JSClassBuilder-1), +so JS calls will directly invoke the compiled delegates. + +Runtime code-generation is used exclusively when +[dynamically invoking .NET APIs from JS](../scenarios/js-dotnet-dynamic.md), since in that scenario +there is no compile step. + +### Compile-time code generation + +The `JSMarshaller` can also be used in the context of a +[C# source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) +to generate marshalling code at compile time. In this scenario, the marshaller still generates +expression trees, but then the expression trees are emitted as C# source code. (Emitting expression +trees as C# code is not a capability provided by the expressions library, but it is not very +difficult to [traverse an expression tree and emit C# syntax for each node](https://github.com/microsoft/node-api-dotnet/blob/main/src/NodeApi.Generator/ExpressionExtensions.cs), especially when the +trees are known to use a limited subset of node types.) + +Compile-time code generation is used when +[building a .NET module for Node.js](../scenarios/js-dotnet-module.md). Referencing the +`Microsoft.JavaScript.NodeApi.Generator` nuget package from a C# project registers the source +generator, which then processes any +[`[JSExport]`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSExportAttribute) or +[`[JSModule]`](../reference/dotnet/Microsoft.JavaScript.NodeApi.Interop/JSModuleAttribute) +attributes in the project being compiled. The generated C# marshalling code is then compiled +into the project output assembly. With this approach, runtime reflection and code generation +are not required, so the startup time is faster compared to dynamic invocation. (However the +performance of subsequent marshalling operations should be the same.) + +The other major benefit of compile-time code generation is that it works with .NET Native AOT, +where runtime reflection is not supported. That makes it possible to +[build a native Node.js module](../scenarios/js-aot-module.md) that does not depend on the .NET +runtime being installed or redistributed. + +## Object lifetimes + +.NET and JavaScript runtimes both use garbage-collection to free up memory after objects are no +longer reachable by the code execution. So when objects are passed between .NET and JS +environments, the marshaller manages the object lifetimes to prevent memory leaks and prevent +objects from being released when they are still referenced from the other side. + +### Marshalling by reference vs by value + +The lifetime management applies to .NET `class` and `interface` types (including collections), +which are marshalled by reference. That means if a .NET `class` instance (including an unknown +class that implements some declared `interface`) is passed to and from JavaScript, then the same +instance is received every time; in other words the values will have reference equality across +multiple marshalling operations. + +The lifetime management and reference equality does not apply to .NET `struct` types, which are +marshalled by value -- even though JavaScript does not have stack-allocated value-types like .NET. +This means every time a .NET `struct` instance is passed to JS, a new temporary JS object is +created, similar to +[.NET boxing](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing). diff --git a/docs/features/js-references.md b/docs/features/js-references.md new file mode 100644 index 00000000..89e2ae19 --- /dev/null +++ b/docs/features/js-references.md @@ -0,0 +1,66 @@ +# JS References + +The [`JSReference`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSReference) class is a strong +or weak reference to a JavaScript value. Use a reference to save a JS value on the .NET heap and +enable it to be accessed later from a different [scope](./js-value-scopes). + +::: warning +The example code below might need to be updated after +https://github.com/microsoft/node-api-dotnet/issues/197 is resolved. +::: + +## Using strong references + +A common practice is to save a reference as a member variable to support using the referenced +JS value in a later callback. A strong reference prevents the JS value from being released +until the reference is disposed. + +```C# +[JSExport] +public class ReferenceExample : IDisposable +{ + private readonly JSReference _dataReference; + + public ReferenceExample(JSArray data) + { + // The constructor must have been invoked from JS, or from .NET + // on the JS thread. Save a reference to the JS value parameter. + _dataReference = new JSReference(data); + } + + public double GetSum() + { + // Access the saved data value via the reference. + // Since the reference is strong, it never returns null. + // (It throws ObjectDisposedException if disposed.) + JSArray data = (JSArray)_dataReference.GetValue()!.Value; + + // JSArray implements IList. + return data.Sum((JSValue value) => (double)value); + } + + public void Dispose() + { + // Disposing the reference releases the JS value, + // if there are no other references to the same value. + _dataReference.Dispose(); + } +} +``` + +## Using weak references + +A weak reference does not prevent the JS value from being released. Therefore it is +necessary to check for null when getting the referenced value: + +```C# +JSValue? value = reference.GetValue(); +if (value != null) +{ + // Do something with the value. +} +else +{ + // The JS value was released and is no longer available. +} +``` diff --git a/docs/features/js-threading-async.md b/docs/features/js-threading-async.md new file mode 100644 index 00000000..4965f83d --- /dev/null +++ b/docs/features/js-threading-async.md @@ -0,0 +1,72 @@ +# JS Threading and Async Continuations + +JavaScript engines have a single-threaded execution model. That means all access to JavaScript +data or operations must be performed from the JavaScript thread. +(It is possible run multiple JavaScript execution threads in a process using +[Web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) or the +[`NodejsEnvironment`](../reference/dotnet/Microsoft.JavaScript.NodeApi.Runtime/NodejsEnvironment) +class, but the thread affinity rules still apply.) + +## Invalid thread access + +All APIs in the `Microsoft.NodeApi.JavaScript` namespace (and child namespaces) must be used on +the JS thread, except where otherwise documented. Any attempt to access JavaScript values or +operations from the wrong thread will throw +[`JSInvalidThreadAccessException`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSInvalidThreadAccessException): + +```C# +[JSExport] +public void InvalidThreadExample() +{ + Thread thread = new(() => + { + var now = new JSDate(); // throws JSInvalidThreadAccessException + }); + thread.Start(); + thread.Join(); +} +``` + +Note that .NET tasks may run on another thread. And accessing a value from another thread is +invalid even if the value's scope has not been closed on the original thread: + +```C# +[JSExport] +public async Task InvalidAsyncExample(JSValue value) +{ + await Task.Run(() => // The lambda runs on a thread-pool thread. + { + var valueString = (string)value; // throws JSInvalidThreadAccessException + }); +} +``` + +## Async continuations + +Accessing JavaScript values after an `await` is valid. This works because the +[`JSSynchronizationContext`](../reference/dotnet/Microsoft.JavaScript.NodeApi.Interop/JSSynchronizationContext) +automatically manages returning to the JavaScript thread. + +```C# +[JSExport] +public async Task ValidAwaitExample(JSValue value) +{ + await HelperMethodAsync(); + // The synchronization context returns to the JS thread after awaiting. + + var valueString = (string)value; // Does not throw! +} +``` + +However, using `ConfigureAwait(false)` disables use of the synchronization context: + +```C# +[JSExport] +public async Task InvalidConfigureAwaitExample(JSValue value) +{ + await HelperMethodAsync().ConfigureAwait(false); + // ConfigureAwait(false) prevents returning to the JS thread. + + var valueString = (string)value; // throws JSInvalidThreadAccessException +} +``` diff --git a/docs/features/js-types-in-dotnet.md b/docs/features/js-types-in-dotnet.md new file mode 100644 index 00000000..f36e8e36 --- /dev/null +++ b/docs/features/js-types-in-dotnet.md @@ -0,0 +1,88 @@ +# JavaScript types in .NET + +While [automatic marshalling](./js-dotnet-marshalling.md) can conveniently convert JavaScript values +to and from almost any specific .NET types, the marshaller has some limitations, particularly +because the [mappings between JS and .NET types](../reference/js-dotnet-types.md) can be inexact. +So sometimes it is necessary for .NET code to interact directly with JavaScript types, precisely +preserving all of the JavaScript type's semantics. + +## The `JSValue` type + +The [`JSValue`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValue) type represents _any_ +kind of JavaScript value. It can be `undefined`, `null`, a primitive (`number`, `string`, etc.) +or an object. The value type can be checked via the +[`JSValue.TypeOf()`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValue/TypeOf) method, +which is equivalent to the JS +[`typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) +operator. `default(JSValue)` is equivalent to the JS `undefined` value. + +The `JSValue` type has methods for doing any kind of operation or conversion on the value, though +many of those may throw a `JSException` if the value is not the correct type. And such an exception +would be [re-thrown](../reference/exceptions) to JS as a `TypeError`. + +Available conversions include casting to and from .NET primitive type: +```C# +JSValue jsString = … +string s = (string)jsString; +JSValue jsString2 = s; // Conversions to JSValue are implicit. +``` + +Internally, a `JSValue` contains only two fields: + - A [`napi_value`](https://nodejs.org/api/n-api.html#napi_value) native handle to the JavaScript + value + - A reference to a [`JSValueScope`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValueScope) + instance, which is primarily a wrapper around a + [`napi_handle_scope`](https://nodejs.org/api/n-api.html#napi_handle_scope) + +`JSValue` is a `struct` because a JavaScript value is not valid outside of its +[scope](./js-value-scopes). (We've considered making it a +[`ref struct`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct) +but that design would have too many limitations.) Being a value-type also reduces memory allocations +when passing JS values to .NET, improving interop performance. + +Use a [`JSReference`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSReference) to store a +JavaScript value on the heap and track it across multiple invocations. + +## Other JavaScript value types +A `JSValue` can be cast to another `struct` that represents a more specific value type: + +```C# +void CopyToStringArray(JSValue jsArray, string[] destination) + => ((JSArray)jsArray).CopyTo(destination, 0, (value) => (string)value); +``` + +The specific value types provide properties and methods specific to their type, and are often easier +to work with: + - [`JSArray`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSArray) + - [`JSBigInt`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSBigInt) + - [`JSDate`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSDate) + - [`JSFunction`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSFunction) + - [`JSIterable`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSIterable) + - [`JSMap`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSMap) + - [`JSObject`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSObject) + - [`JSPromise`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSPromise) + - [`JSProxy`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSProxy) + - [`JSSet`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSSet) + - [`JSSymbol`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSSymbol) + - [`JSTypedArray`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSTypedArray-1) + +The value types all implement `IEquatable`, which evaluates JavaScript strict equality. + +The .NET `ToString()` method of any type of JS value returns the same result as calling `toString()` +from JavaScript. + +## Using JavaScript value types in .NET APIs + +When exporting a .NET method to JavaScript, use +[`JSCallbackArgs`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSCallbackArgs) to declare a +method that can accept any number of JavaScript values as arguments: +```C# +[JSExport] +public JSValue ExampleWithVariableJSArgs(JSCallbackArgs args) { … } +``` + +Mixing .NET types and JS types in a method signature is also allowed: +```C# +[JSExport] +public JSValue ExampleWithJSArray(string s, JSValue value, JSArray array) { … } +``` diff --git a/docs/features/js-value-scopes.md b/docs/features/js-value-scopes.md new file mode 100644 index 00000000..de332411 --- /dev/null +++ b/docs/features/js-value-scopes.md @@ -0,0 +1,74 @@ +# JavaScript Value Scopes + +A JavaScript value in .NET is always associated with a value scope via its +[`JSValue.Scope`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValue/Scope) property, +which returns a [`JSValueScope`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValueScope). +A value is only valid within its scope; if the scope is closed (disposed), then attempts to +access or use the value will throw +[`JSValueScopeClosedException`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValueScopeClosedException). + +Values received by a .NET method that is a JS callback are associated with a `Callback` +[scope type](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValueScopeType). When the method +returns, the callback scope is closed and any values in that scope become invalid. + +## Nesting and escaping scopes + +To limit the lifetime of JavaScript values, use a nested scope. This can be particularly important +if values are created in many iterations of a loop. Without a nested scope, none of the values +created within the loop would be released until the method returns, which could use a lot of memory. + +```C# +string[] array = … +JSFunction jsFunction = … + +foreach (string item in array) +{ + using (var nestedScope = new JSValueScope()) + { + // Passing a .NET string to JS requires converting it to JSValue. + // The conversion is implicit; the explicit cast is for illustration. + jsFunction.Call(thisArg: default, (JSValue)item); + } +} +``` + +Use an _escapable_ scope when it's necessary to return a value out of a nested scope. An escapable +scope allows one (and only one) value to be promoted to the containing scope: + +```C# +[JSExport] +public JSValue EscapableScopeExample(JSCallbackArgs args) +{ + string[] array = … + JSFunction jsFunction = … + + foreach (string item in array) + { + using (var escapableScope = new JSValueScope(JSValueScopeType.Escapable)) + { + JSValue result = jsFunction.Call(thisArg: default, (JSValue)item); + if (!result.IsUndefined()) + { + // If the result is not escaped, it would be released when + // the inner scope is disposed (before the method returns). + return escapableScope.Escape(result); + } + } + } + + return default; +} +``` + +## Scope thread affinity + +JavaScript values and value scopes can only be accessed from the JavaScript main thread, which +is the thread that invokes the .NET callback. An attempt to access a value or scope from a +different thread will throw +[`JSInvalidThreadAccessException`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSInvalidThreadAccessException). +For more details, see [JS Threading and Async Continuations](./js-threading-async). + +## References + +To save a value for later use in another scope, create a reference to it and save the reference. +See [JS References](./js-references). diff --git a/docs/features/performance.md b/docs/features/performance.md new file mode 100644 index 00000000..72479887 --- /dev/null +++ b/docs/features/performance.md @@ -0,0 +1,20 @@ +# Performance + +.NET / JS interop is fast because: + - Marshaling does not use JSON serialization. + - Compile-time or runtime [code generation](./js-dotnet-marshalling#marshalling-code-generation) + avoids reflection. + - Use of shared memory and proxies minimizes data transfer. + - Use of modern C# like `struct`, `Span`, and `stackalloc` minimizes heap allocation & copying. + +## Performance comparison vs Edge.js +Warm JS to .NET calls are nearly twice as fast when compared to +[`edge-js`](https://github.com/agracio/edge-js) using +[that project's benchmark](https://github.com/tjanczuk/edge/wiki/Performance). + +| | HTTP | Edge.js | Node API .NET | AOT | JS (baseline) | +|-----:|------:|--------:|--------------:|----:|--------------:| +| Cold | 32039 | 38761 | 9355 | 362 | 1680 | +| Warm | 2003 | 87 | 54 | 47 | 23 | + +Numbers are _microseconds_. "Warm" is an average of 10000 .NET -> JS calls (passing a medium-size object). diff --git a/docs/features/type-definitions.md b/docs/features/type-definitions.md new file mode 100644 index 00000000..5e1510f5 --- /dev/null +++ b/docs/features/type-definitions.md @@ -0,0 +1,57 @@ +# TypeScript Type Definitions + +Type definitions enable JavaScript or TypeScript code to benefit from compile-time type checking, +editor suggestions, and documentation tips while working with .NET APIs. The type definitions can be +automatically generated for any .NET project or pre-existing NuGet packages, using either MSBuild +or a stand-alone command-line tool. + +Many transformations are applied to .NET APIs when they are projected to JavaScript, in order to +accomodate differences in the type systems, runtime libraries, and common conventions between .NET +and JavaScript. The generated type definitions are the compile-time declarations that correspond +to the behavior of the JavaScript marshaller, which is the runtime component responsible for +actually converting method calls, parameters, return types, etc. between the two runtime +environments. For details, see [Marshalling between .NET and JavaScript](./js-dotnet-marshalling) +and the [JS / .NET type mappings reference](../reference/js-dotnet-types). + +## Generating type definitions with MSBuild + +The easiest way to generate type definitions is to leverage the provided MSBuild targets. The +`Microsoft.JavaScript.NodeApi.Generator.targets` file is automatically imported when referencing +the `Microsoft.JavaScript.NodeApi.Generator` NuGet package: +```xml + + + +``` + +By default, the imported targets will generate type definitions only for the _current_ project. +But if the current project is empty (contains no `Compile` items), then type definitions are +generated for all assemblies referenced by the project, including both NuGet package assemblies +and system assemblies. This use of an empty project enables leveraging MSBuild to restore packages +and generate type definitions, for the purpose of dynamically invoking those packages from +JavaScript without writing a C# module. See +[Dynamically invoke .NET APIs from JavaScript](../scenarios/js-dotnet-dynamic). + +To customize some aspects of generating type definitions via MSBuild, see the +[MSBuild properties](../reference/msbuild-props) reference. + +## Generating type definitions with the command-line tool + +The `node-api-dotnet-generator` npm package is a standalone command-line tool that wraps the +`Microsoft.JavaScript.NodeApi.Generator` assembly and enables using it outside of MSBuild. + +| Parameter | Alias | Description | +|---------------|-------|-------------| +| `--asssembly` | `-a` | Required path to the assembly file for which type definitions are to be generated. +| `--framework` | `-f` | Target framework moniker of system assembly dependencies, e.g. `net8.0`. Defaults to the .NET runtime version used when invoking the tool (currently .NET 8). +| `--pack` | `-p` | Application targeting pack(s) to check when resolving system assembly dependencies. Defaults to `Microsoft.NETCore.App`. May be specified more than once. Add the `Microsoft.AspNetCore.App` targeting pack for an ASP.NET app, or `Microsoft.WindowsDesktop.App` for a Windows desktop app. +| `--reference` | `-r` | Path to an assembly that is referenced by the primary assembly. System assemblies are located automatically via the targeting packs and do not need to be specified separately. All other referenced assemblies must be provided. May be specified more than once. +| `--typedefs` | `-t` | Required path to output generated type definitions (`.d.ts`) file. +| `--module` | `-m` | Generate a JS module-loader script alongside the typedefs. May be specified more than once. Each value is either `commonjs` or `esm`, or a path to a `package.json` file with a `"type"` property specifying the module type. +| `--nowarn` | | Do not display warnings about APIs that cannot be projected to JavaScript. +| `--help` | `-h` `-?` | Show command-line help. +| @<file>          || Read the specified response file for more options. Typically used when a long list of reference assembly paths may exceed the maximum command-line length. + +Note each invocation generates type definitions only for one specified primary assembly, even when +multiple reference assemblies are also provided. So generating type definitions for all reference +assemblies (and system assemblies) may require many invocations. diff --git a/Docs/generics.md b/docs/generics.md similarity index 100% rename from Docs/generics.md rename to docs/generics.md diff --git a/docs/images/dark/ts.svg b/docs/images/dark/ts.svg new file mode 100644 index 00000000..652f930e --- /dev/null +++ b/docs/images/dark/ts.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/Docs/images/dotnet-bot_scene_coffee-shop.png b/docs/images/dotnet-bot_scene_coffee-shop.png similarity index 100% rename from Docs/images/dotnet-bot_scene_coffee-shop.png rename to docs/images/dotnet-bot_scene_coffee-shop.png diff --git a/docs/images/light/ts.svg b/docs/images/light/ts.svg new file mode 100644 index 00000000..372853ac --- /dev/null +++ b/docs/images/light/ts.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/docs/images/node-api-dotnet-logo.svg b/docs/images/node-api-dotnet-logo.svg new file mode 100644 index 00000000..19bce26b --- /dev/null +++ b/docs/images/node-api-dotnet-logo.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..fa84c9c8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,65 @@ +--- +# Markdown docs in this directory are rendered to HTML using vitepress: https://vitepress.dev/ +# Browse the rendered documentation at https://microsoft.github.io/node-api-dotnet + +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: Node API for .NET + tagline: Advanced interoperability between .NET and JavaScript in the same process + image: + src: /images/node-api-dotnet-logo.svg + alt: Node API .NET logo + actions: + - theme: alt + text: Overview + link: /overview + - theme: brand + text: Get Started + link: /scenarios/index + - theme: alt + text: Features + link: /features/type-definitions + - theme: alt + text: JS / .NET Mappings + link: /reference/js-dotnet-types1 + - theme: alt + text: JS API Reference + link: /reference/js/ + - theme: alt + text: .NET API Reference + link: /reference/dotnet/ + +features: + - title: Call .NET from JS + icon: 🔃 + link: /scenarios/js-dotnet-dynamic + details: Load .NET assemblies from JavaScript and use nearly any APIs. + - title: Call JS from .NET + icon: 🔃 + link: /scenarios/dotnet-js + details: Run Node.js or another JS runtime in a .NET application, with advanced interop capabilities. + - title: Type definitions + icon: + dark: /images/dark/ts.svg + light: /images/light/ts.svg + link: /features/type-definitions + details: Automatically generate TypeScript type definitions for .NET assemblies. + - title: .NET Native AOT support + icon: 🤖 + link: /features/dotnet-native-aot + details: Optionally compile a C# library to a fully native Node.js addon that does not depend on the .NET runtime. + - title: Automatic marshalling + icon: 🏭 + link: /features/js-dotnet-marshalling + details: Pass classes, collections, streams, and more seamlessly between JS and .NET. + - title: High performance + icon: 🚀 + link: /features/performance + details: Build-time source-generation or runtime code-generation optimizes interop performance. + - title: Exception propagation + icon: 💣 + link: /reference/exceptions + details: .NET exceptions convert to/from JS errors, with combined stack traces. +--- diff --git a/Docs/masrshalling-null-undefined.md b/docs/masrshalling-null-undefined.md similarity index 100% rename from Docs/masrshalling-null-undefined.md rename to docs/masrshalling-null-undefined.md diff --git a/Docs/node-module.md b/docs/node-module.md similarity index 92% rename from Docs/node-module.md rename to docs/node-module.md index 70685357..0ab160f4 100644 --- a/Docs/node-module.md +++ b/docs/node-module.md @@ -1,7 +1,8 @@ ## Develop a Node module in C# -For a minimal example of this scenario, see -[../examples/dotnet-module/](../examples/dotnet-module/). +For a minimal example of this scenario, see the +[dotnet-module](https://github.com/microsoft/node-api-dotnet/tree/main/examples/dotnet-module) +project. 1. Create a .NET Class library project that targets .NET 6 or later. (.NET 8 for AOT.) ``` @@ -16,9 +17,6 @@ For a minimal example of this scenario, see dotnet add package --prerelease Microsoft.JavaScript.NodeApi dotnet add package --prerelease Microsoft.JavaScript.NodeApi.Generator ``` - > :warning: Until these packages are published, you'll need to - [build them from source](../README-DEV.md).
Then add the `out/pkg` directory as a local - package source in your `NuGet.config`. Afterward you should have the two references in your project file: ```xml @@ -55,9 +53,6 @@ For a minimal example of this scenario, see ``` npm install node-api-dotnet ``` - > :warning: Until this package is published, you'll need to - [build it from source](../README-DEV.md).
Then get the package from - `out/pkg/node-api-dotnet-{version}.tgz`. 6. Import the `node-api-dotnet` package in your JavaScript or TypeScript code. The import syntax depends on the [module system](https://nodejs.org/api/esm.html) the current project is using. diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000..3158ef65 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,37 @@ +# Project Overview + +This project enables advanced interoperability between .NET and JavaScript in the same process. + + - Load .NET assemblies and call .NET APIs in-proc from a JavaScript application. + - Load JavaScript packages call JS APIs in-proc from a .NET application. + +Interop is [high-performance](./features/performance) and supports [TypeScript type-definitions +generation](./features/type-definitions), [async (tasks/promises)](./features/js-threading-async), +[streams](./reference/streams), [exception propagation](./reference/exceptions), and more. It is +built on [Node API](https://nodejs.org/api/n-api.html) so it is compatible with any Node.js version +(without recompiling) or other JavaScript runtime that supports Node API, such as Deno. + +:warning: _**Status: Public Preview** - Most functionality works well, though there are some known +limitations around the edges, and there may still be minor breaking API changes._ + +### Minimal example - JS calling .NET +```JavaScript +const Console = require('node-api-dotnet').System.Console; +Console.WriteLine('Hello from .NET!'); +``` + +### Minimal example - .NET calling JS +```C# +interface IConsole { void Log(string message); } + +var nodejs = new NodejsPlatform(libnodePath).CreateEnvironment(); +nodejs.Run(() => { + var console = nodejs.Import("global", "console"); + console.Log("Hello from JS!"); +}); +``` + +For more complete example projects, see the +[examples directory in the repo](https://github.com/microsoft/node-api-dotnet/tree/main/examples). +Or proceed to the next page to learn about the different supported scenarios and how to get started +with your own project. \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..bb177038 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,1776 @@ +{ + "name": "node-api-dotnet-docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-api-dotnet-docs", + "devDependencies": { + "typedoc": "^0.25.13", + "vitepress": "^1.2" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", + "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", + "@algolia/autocomplete-shared": "1.9.3" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", + "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", + "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", + "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz", + "integrity": "sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.22.1" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.22.1.tgz", + "integrity": "sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA==", + "dev": true + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz", + "integrity": "sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.22.1" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.22.1.tgz", + "integrity": "sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/client-search": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.22.1.tgz", + "integrity": "sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/client-search": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.22.1.tgz", + "integrity": "sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.22.1.tgz", + "integrity": "sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-search": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.22.1.tgz", + "integrity": "sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/logger-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.22.1.tgz", + "integrity": "sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg==", + "dev": true + }, + "node_modules/@algolia/logger-console": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.22.1.tgz", + "integrity": "sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA==", + "dev": true, + "dependencies": { + "@algolia/logger-common": "4.22.1" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz", + "integrity": "sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.22.1" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.22.1.tgz", + "integrity": "sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg==", + "dev": true + }, + "node_modules/@algolia/requester-node-http": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz", + "integrity": "sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.22.1" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.22.1.tgz", + "integrity": "sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.22.1", + "@algolia/logger-common": "4.22.1", + "@algolia/requester-common": "4.22.1" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.0.tgz", + "integrity": "sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.6.0.tgz", + "integrity": "sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.6.0", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.6.0.tgz", + "integrity": "sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.9.3", + "@algolia/autocomplete-preset-algolia": "1.9.3", + "@docsearch/css": "3.6.0", + "algoliasearch": "^4.19.1" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.9.0.tgz", + "integrity": "sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==", + "dev": true + }, + "node_modules/@shikijs/transformers": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.9.0.tgz", + "integrity": "sha512-wo8dNbZtFtVhKtw8BnXIT/FDTGMwEdWcQSIRa78ou14JGkMYxSCBN942W5+IRUifP5BwVUWgkXBYX/M3FUFkeg==", + "dev": true, + "dependencies": { + "shiki": "1.9.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz", + "integrity": "sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", + "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.29", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", + "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.4.29", + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", + "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.29", + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", + "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.4.29", + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.3.3.tgz", + "integrity": "sha512-Q9SpC5puX+VZ8mAixoVHAYRgLij4e/MCEfXze4gScdtxQuemHq7/zEE0EsgiwSrISaxwNGooymU8UO78W2yA7Q==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.3.3" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.3.tgz", + "integrity": "sha512-m+dFI57BrzKYPKq73mt4CJ5GWld5OLBseLHPHGVP7CaILNY9o1gWVJWAJeF8XtQ9LTiMxZSaK6NcBsFuxAhD0g==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.3.3", + "birpc": "^0.2.17", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.3.tgz", + "integrity": "sha512-JQTVDM1WE4RRAsHTZvAB7+jczdHcbHgNUU/MWSIx5sV7FzCmnyWETPQ7K4/Y477weqT5xbybtBaK/OpzdaCiBQ==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", + "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.4.29", + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", + "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.4.29", + "@vue/runtime-core": "3.4.29", + "@vue/shared": "3.4.29", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", + "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29" + }, + "peerDependencies": { + "vue": "3.4.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz", + "integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.0", + "@vueuse/shared": "10.11.0", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.11.0.tgz", + "integrity": "sha512-Pp6MtWEIr+NDOccWd8j59Kpjy5YDXogXI61Kb1JxvSfVBO8NzFQkmrKmSZz47i+ZqHnIzxaT38L358yDHTncZg==", + "dev": true, + "dependencies": { + "@vueuse/core": "10.11.0", + "@vueuse/shared": "10.11.0", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^4", + "drauu": "^0.3", + "focus-trap": "^7", + "fuse.js": "^6", + "idb-keyval": "^6", + "jwt-decode": "^3", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^6" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz", + "integrity": "sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.0.tgz", + "integrity": "sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.22.1.tgz", + "integrity": "sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==", + "dev": true, + "dependencies": { + "@algolia/cache-browser-local-storage": "4.22.1", + "@algolia/cache-common": "4.22.1", + "@algolia/cache-in-memory": "4.22.1", + "@algolia/client-account": "4.22.1", + "@algolia/client-analytics": "4.22.1", + "@algolia/client-common": "4.22.1", + "@algolia/client-personalization": "4.22.1", + "@algolia/client-search": "4.22.1", + "@algolia/logger-common": "4.22.1", + "@algolia/logger-console": "4.22.1", + "@algolia/requester-browser-xhr": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/requester-node-http": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/birpc": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.17.tgz", + "integrity": "sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/minisearch": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", + "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==", + "dev": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.19.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.6.tgz", + "integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz", + "integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==", + "dev": true, + "peer": true + }, + "node_modules/shiki": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.9.0.tgz", + "integrity": "sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==", + "dev": true, + "dependencies": { + "@shikijs/core": "1.9.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "node_modules/typedoc": { + "version": "0.25.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", + "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.7" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typedoc/node_modules/shiki": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", + "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.2.3.tgz", + "integrity": "sha512-GvEsrEeNLiDE1+fuwDAYJCYLNZDAna+EtnXlPajhv/MYeTjbNK6Bvyg6NoTdO1sbwuQJ0vuJR99bOlH53bo6lg==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.6.0", + "@docsearch/js": "^3.6.0", + "@shikijs/core": "^1.6.2", + "@shikijs/transformers": "^1.6.2", + "@types/markdown-it": "^14.1.1", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/devtools-api": "^7.2.1", + "@vue/shared": "^3.4.27", + "@vueuse/core": "^10.10.0", + "@vueuse/integrations": "^10.10.0", + "focus-trap": "^7.5.4", + "mark.js": "8.11.1", + "minisearch": "^6.3.0", + "shiki": "^1.6.2", + "vite": "^5.2.12", + "vue": "^3.4.27" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, + "node_modules/vue": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", + "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-sfc": "3.4.29", + "@vue/runtime-dom": "3.4.29", + "@vue/server-renderer": "3.4.29", + "@vue/shared": "3.4.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..1e3c2862 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "node-api-dotnet-docs", + "devDependencies": { + "typedoc": "^0.25.13", + "vitepress": "^1.2" + }, + "scripts": { + "build-dotnet": "node ./tools/build-dotnet-api-docs.js", + "build-js": "node ./tools/build-js-api-docs.js", + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + } +} diff --git a/Docs/presentation.html b/docs/presentation.html similarity index 100% rename from Docs/presentation.html rename to docs/presentation.html diff --git a/Docs/presentation.md b/docs/presentation.md similarity index 100% rename from Docs/presentation.md rename to docs/presentation.md diff --git a/Docs/presentation2.html b/docs/presentation2.html similarity index 100% rename from Docs/presentation2.html rename to docs/presentation2.html diff --git a/Docs/presentation2.md b/docs/presentation2.md similarity index 100% rename from Docs/presentation2.md rename to docs/presentation2.md diff --git a/docs/reference/arrays-collections.md b/docs/reference/arrays-collections.md new file mode 100644 index 00000000..0b7c3e5b --- /dev/null +++ b/docs/reference/arrays-collections.md @@ -0,0 +1,83 @@ +# Arrays and Collections + +## Arrays + +| C# Type | JS Type | +|----------|--------------------| +| `T[]` | `T[]` (`Array`) | + +.NET arrays are marshalled by value to or from JS. (This would not be the preferred design, but +unfortunately there is no way to create a .NET array over "external" memory, that is memory not +allocated / managed by the .NET runtime.) This means that whenever a .NET array instqance is +marshalled to or from JS, all the array items are copied. Use `IList` or another collection +interface to avoid copying the items. + +## Collections + +| C# Type | JS Type | +|-----------------------------|-----------------------| +| `IEnumerable` | `Iterable` | +| `IAsyncEnumerable` | `AsyncIterable` | +| `IReadOnlyCollection` | `ReadonlySet` | +| `ICollection` | `Set` | +| `IReadOnlySet` | `ReadonlySet` | +| `ISet` | `Set` | +| `IReadOnlyList` | `readonly T[]` (`ReadonlyArray`) | +| `IList` | `T[]` (`Array`) | +| `IReadOnlyDictionary` | `ReadonlyMap` | +| `IDictionary` | `Map` | +| `KeyValuePair`| `[TKey, TValue]` | + +Collections (other than .NET arrays) are marshalled by reference. This means passing an instance of +a collection between .NET and JS does not immediately copy any values, and any modifications to the +collection affect both .NET and JS. + +JavaScript collections can be adapted to .NET collection interfaces using the extension methods +in [`JSCollectionExtensions`](./dotnet/Microsoft.JavaScript.NodeApi.Interop/JSCollectionExtensions). + +Concrete collection classes like `List`, `Dictionary`, `ReadOnlyCollection` are +[not yet implemented](https://github.com/microsoft/node-api-dotnet/issues/242). If and when +they are supported they will have major limitations so are not recommended. Use interfaces instead, +which is standard practice for public APIs anyway. + +Non-generic collection interfaces in the `System.Collections` namespace are +[not yet implemented](https://github.com/microsoft/node-api-dotnet/issues/243), since they are +rarely used in modern C# code. + +## Typed arrays + +| C# Type | JS Type | +|------------------|------------------| +| `Memory` | `Int8Array` | +| `Memory` | `UInt8Array` | +| `Memory` | `Int16Array` | +| `Memory` | `UInt16Array` | +| `Memory` | `Int32Array` | +| `Memory` | `UInt32Array` | +| `Memory` | `BigInt64Array` | +| `Memory` | `BigUInt64Array` | +| `Memory` | `Float32Array` | +| `Memory` | `Float64Array` | +| `ReadOnlyMemory` | `Int8Array` | +| `ReadOnlyMemory` | `UInt8Array` | +| `ReadOnlyMemory` | `Int16Array` | +| `ReadOnlyMemory` | `UInt16Array` | +| `ReadOnlyMemory` | `Int32Array` | +| `ReadOnlyMemory` | `UInt32Array` | +| `ReadOnlyMemory` | `BigInt64Array` | +| `ReadOnlyMemory` | `BigUInt64Array` | +| `ReadOnlyMemory` | `Float32Array` | +| `ReadOnlyMemory` | `Float64Array` | + +A JavaScript [typed array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays) +represents a contiguous range of bytes or other type of numeric values. In .NET, similar ranges are +represented using `Memory`, or `ReadOnlyMemory` where `T` is a primitive numeric type. (Other +types for `T`, such as a `struct`, are not supported by the JS marshaller.) + +When a typed array is marshalled between .NET and JS, the memory becomes _shared_. Only the memory +location and length are copied by the marshaller; the memory contents are not. Any modifications +(assuming it is not read-only) are seen by both sides. The memory will be garbage-collected when +no longer reachable by either side. + +[`JSTypedArray`](./dotnet/Microsoft.JavaScript.NodeApi/JSTypedArray-1) supports working directly +with JS typed-array values, and provides the conversions to/from `Memory`. diff --git a/docs/reference/async-promises.md b/docs/reference/async-promises.md new file mode 100644 index 00000000..b75e002f --- /dev/null +++ b/docs/reference/async-promises.md @@ -0,0 +1,42 @@ +# Async, Tasks, and Promises + +| C# Type | JS Type | +|-----------|-----------------| +| `Task` | `Promise` | +| `Task` | `Promise` | + +## Async methods + +A .NET method that returns a `Task` or `Task` can be awaited in JavaScript because the +`Task` is automatically marshalled as a JS `Promise`: + +```C# +[JSExport] +public static class AsyncExample +{ + public async Task GetResultAsync(); +} +``` + +```JS +const result = await AsyncExample.getResultAsync(); +``` + +A JavaScript method that returns a `Promise` can be awaited in C# by converting the `Promise` to a +`Task`: + +```C# +JSFunction exampleAsyncJSFunc = … +string result = await exampleAsyncJSFunc.CallAsStatic(arg).AsTask(); +``` + +## `JSPromise` type + +The [`JSPromise`](./dotnet/Microsoft.JavaScript.NodeApi/JSPromise) type supports working directly +with JavaScript `Promise` values, while the extension methods in the +[`JSPromiseExtensions`](./dotnet/Microsoft.JavaScript.NodeApi/TaskExtensions) +class enable converting between `JSPromise` and `Task` values. + +## Async execution +See [JS Threading and Async Continuations](../features/js-threading-async) for more about +coordinating asynchronous .NET and JavaScript execution. diff --git a/docs/reference/basic-types.md b/docs/reference/basic-types.md new file mode 100644 index 00000000..e6229030 --- /dev/null +++ b/docs/reference/basic-types.md @@ -0,0 +1,91 @@ +# Basic Types + +| C# Type | JS Type | +|----------|----------| +| `string` | `string` | +| `bool` | `boolean`| +| `sbyte` | `number` | +| `byte` | `number` | +| `short` | `number` | +| `ushort` | `number` | +| `int` | `number` | +| `uint` | `number` | +| `long` | `number` | +| `ulong` | `number` | +| `float` | `number` | +| `double` | `number` | +| `nint` | _not yet implemented_ | +| `nuint` | _not yet implemented_ | +| `decimal`| [_not yet implemented_](https://github.com/microsoft/node-api-dotnet/issues/316) | + +## Conversions between .NET and JS primitive types + +C# primitive types can be implicitly converted to +[`JSValue`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSValue). +Conversions from `JSValue` to C# primitives are explicit. These explicit conversions _do not_ do +type coercion, so they may throw `JSException` if the JS value is not the expected type. Use +one of the `CoerceTo*` methods if JS type coercion is desired. + +```C# +string dotnetString = "22"; +JSValue jsString = dotnetString; // implicit conversion +string dotnetString2 = (string)jsString; // explicit conversion +int dotnetInt = (int)jsString.CoerceToNumber(); // 22 +int invalidInt = (int)jsString; // throws JSException: A number was expected +``` + +## Nullable conversions + +Nullable conversions are also supported, in case the JS value might be `null` or `undefined`: + +```C# +JSValue maybeJsString = JSValue.Null; +string? maybeString = (string?)maybeJsString; // JS null => .NET null +JSValue maybeJsString2 = maybeString; // .NET null => JS undefined + +JSValue maybeJsNumber = JSValue.Undefined; +int? maybeInt = (int?)maybeJsNumber; // JS undefined => .NET null +JSValue maybeJsNumber2 = maybeInt; // .NET null => JS undefined +``` + +See also [Null and Undefined](./null-undefined). + +## Strings + +Converting between a .NET `string` and a JavaScript `string` unfortunately internally allocates +memory and makes a copy of the string, because neither runtime allows a string to point to +"external" memory (memory that isn't allocated and tracked by the same runtime). So be aware that +frequently passing large strings between .NET and JS can have a performance impact. If appropriate, +consider alternatives such as a [stream](./streams) or +[`Memory`](./arrays-collections#typed-arrays). + +### String encoding + +JavaScript string APIs support getting and setting string values using either UTF-8 or UTF-16, while +.NET strings use UTF-16 internally. When working with UTF-8 encoded strings, it is more efficient +to directly read or write UTF-8 bytes from/to the JavaScript value without using a .NET `string` +as an intermediate value. See +[`JSValue.GetValueStringUtf8()`](./dotnet/Microsoft.JavaScript.NodeApi/JSValue/GetValueStringUtf8) +and [`JSValue.CreateStringUtf8()`](./dotnet/Microsoft.JavaScript.NodeApi/JSValue/CreateStringUtf8). + +## Numeric types + +All .NET numeric types are convertable to and from the JS `number` type. That means there can be +some loss in precision in either direction, except when converting between `double` and `number` +which are both 64-bit IEEE-754 values. If lossiness is a concern, use a +[`checked`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/checked-and-unchecked) +conversion to or from `double` before converting to or after converting from `JSValue`: + +```C# +JSValue jsNumber = … +double doubleValue = (double)jsNumber; +int intValue = checked((int)doubleValue); +``` + +```C# +long longValue = … +double doubleValue = checked((double)longValue); +JSValue jsNumber = doubleValue; // Implicit conversion from double to JSValue. +``` + +See also [BigInteger](./other-types#biginteger). \ No newline at end of file diff --git a/docs/reference/classes-interfaces.md b/docs/reference/classes-interfaces.md new file mode 100644 index 00000000..020a3616 --- /dev/null +++ b/docs/reference/classes-interfaces.md @@ -0,0 +1,117 @@ +# Classes and Interfaces + +## Export a .NET class to JavaScript + +C# code in a [.NET Module for Node.js](../scenarios/js-dotnet-module) can export classes to JS. +When a class is exported, all `public` constructors, properties, and methods of the class are +made available to JavaScript (including static and non-static members). Marshalling code for +the exported class and members is +[generated at compile time](../features/js-dotnet-marshalling#compile-time-code-generation). + +```C# +// Assembly: ExampleLibrary +namespace Microsoft.JavaScript.NodeApi.Examples; + +[JSExport] +public class ExampleClass +{ + public ExampleClass(string name) { Name = name; } + public string Name { get; set; } + public string Hello() => $"Hello {Name}!"; +} +``` +```JS +import { ExampleClass } from './bin/ExampleLibrary.js'; // generated +const example = new ExampleClass('.NET'); +const name = example.name; // returns ".NET" +const result = example.hello(); // returns "Hello .NET!" +``` + +Note the class's namespace is not projected to JavaScript, rather the class is exported directly +from the assembly module. Also the class's properties and methods are automatically converted to +camel-case when the class is exported. This makes it easier to develop modules that follow both +C# and JavaScript naming conventions. + +## Dynamically import a .NET assembly and class + +JavaScript code can use the `node-api-dotnet` package to +[dynamically load .NET assemblies and classes](../scenarios/js-dotnet-dynamic). This is typically +used when the assembly is already built (often acquired via a nuget package), rather than +custom-developed for JS interop as in the previous example. + +```JS +import dotnet from 'node-api-dotnet'; +import './bin/Microsoft.SemanticKernel.Core.js'; // generated +import './bin/Microsoft.SemanticKernel.Connectors.OpenAI.js'; // generated + +const kernelBuilder = dotnet.Microsoft.SemanticKernel.Kernel.CreateBuilder(); +kernelBuilder.AddAzureOpenAIChatCompletion(…); +``` + +When dyanmically importing APIs like this, the imported types are fully namespaced, and members are +_not_ camel-cased. Importing each assembly merges the namespaces and types from the assembly onto +the `dotnet` object. (The types are actually delay-loaded, meaning the +[marshalling](../features/js-dotnet-marshalling#runtime-code-generation) code for each type is not +generated until first use.) + +See the [Semantic Kernel](https://github.com/microsoft/node-api-dotnet/tree/main/examples/semantic-kernel) +project for a more complete example of generating type definitions for nuget packages, loading the +assemblies, and dynamically calling the APIs. + +## Marshalling .NET classes to JS + +.NET class instances are marshalled by reference. That means that when an instance of .NET class is +constructed by JS or passed from .NET to JS, the instance data is not copied. Instead, the JS value +created by the marshaller is a proxy to the .NET value. Any calls to properties or methods on the +JS object get marshalled over to the original .NET object, and the .NET return value is marshalled +back to JS. And when the JS proxy object is garbage-collected, the .NET object can also be +garbage-collected (if there are no other .NET references to the object). + +The JS object is not a actually JavaScript +[`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), +rather it is an instance of a JavaScript class (registered with +[`DefineClass()`](./dotnet/Microsoft.JavaScript.NodeApi.Interop/JSClassBuilder-1/DefineClass)) +whose property getters & setters and methods all have .NET callback functions. + +## Implement a .NET interface with a JS class + +The [TypeScript type-definitions generator](../features/type-definitions.md) converts a .NET +interface into a TypeScript interface. Then a JavaScript (TypeScript) class can implement +implement the interface, and an instance of that JS class can be passed to a .NET API that +expects an instance of the interface. + +```C# +// Assembly: ExampleLibrary + +[JSExport] +public interface IExampleInterface +{ + void CallBack(string value); +} + +[JSExport] +public class ExampleClass +{ + public static string CallMeBack(IExampleInterface caller, string value) + => caller.CallBack(value); +} +``` + +```JS +import { + IExampleInterface, + ExampleClass, +} from './bin/ExampleLibrary.js'; // generated + +class ExampleImplementation extends IExampleInterface { + public void callBack(value: string) { + console.log(`callBack(${value})`); + } +} + +ExampleClass.callMeBack(new ExampleImplementation()); + +``` + +This is one way for .NET to call back into JavaScript. Another way is to +[provide a JS function as a .NET delegate](./delegates). diff --git a/docs/reference/dates.md b/docs/reference/dates.md new file mode 100644 index 00000000..db577d14 --- /dev/null +++ b/docs/reference/dates.md @@ -0,0 +1,66 @@ + +# Date and Time Types + +| C# Type | JS Type | +|------------------|--------------------------------------------------------| +| `DateTime` | `Date \| { kind?: 'utc' \| 'local' \| 'unspecified' }` | +| `DateTimeOffset` | `Date \| { offset?: number }` | +| `TimeSpan` | `number` (milliseconds) | + +## JS Date / .NET DateTime & DateTimeOffset +There is not a clean mapping between the built-in types for dates and times in .NET and JS. +In particular the built-in JavaScript `Date` class has very limited and somewhat confusing +functionality. For this reason many applications use the popular `moment.js` library, but this +project prefers to avoid depending on an external library. Also [a new "Temporal" API is +proposed for standardization](https://tc39.es/proposal-temporal/docs/), but it is not widely +available yet. + +A JavaScript [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) +object is fundamentally a wrapper around a single primitive numeric value that is a UTC timestamp +(milliseconds since the epoch). It does not hold any other state related to offsets or time zones. +This UTC primitive value is returned by the `Date.valueOf()` function, and is one type of value +accepted by the `Date()` constructor. The confusing thing about it is that other constructors and +most `Date` methods operate in the context of the current local time zone, automatically converting +to/from UTC as needed. That includes the default `toString()` method, though alternatives like +`toUTCString()` and `toISOString()` can get the time in UTC instead. + +In .NET, both `DateTime` and `DateTimeOffset` are widely used. While the latter is more modern +and fully-featured, the simpler `DateTime` is still sufficient for many scenarios. So for best +interoperability, both types of .NET values are convertible to and from JS `Date` values. This is +accomplished by adding either a `kind` or `offset` property to a regular `Date` object. + +```TypeScript +type DateTime = Date | { kind?: 'utc' | 'local' | 'unspecified' } +``` + +When a .NET `DateTime` is marshalled to a JS `Date`, the date's UTC timestamp value becomes the +JS `Date` value, regardless of the `DateTime.Kind` property. Then the `kind` property is added to +facilitate consistent round-tripping, so that a `DateTime` can be passed from .NET to JS and +back to .NET without its `Kind` changing. (As noted above, the JS `Date.toString()` always +displays local time, and that remains true even for a `Date` with `kind == 'utc'`.) If a +regular JS `Date` object without a `kind` hint is marshalled to .NET, it becomes a `DateTime` +with `Utc` kind. (Defaulting to `Unspecified` would be more likely to result in undesirable +conversions to/from local-time.) + +```TypeScript +type DateTimeOffset = Date | { offset?: number } +``` + +When a .NET `DateTimeOffset` is marshalled to a JS `Date`, the UTC timestamp value _without the +offset_ becomes the JS `Date` value. Then the `offset` property is added to the object. The +`offset` is a positive or negative integer number of minutes, equivalent to +[DateTimeOffset.TotalOffsetMinumtes](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.totaloffsetminutes). +Additionally, the `toString()` method of the `Date` object is overridden such that it displays +the time _with the offset applied_, followed by the offset, in the form +`YYYY-MM-DD HH:mm:SS (+/-)HH:mm`, consistent with how .NET `DateTimeOffset` is displayed. +If a regular JS `Date` object without an `offset` value is marshalled to .NET, it becomes a +`DateTimeOffset` with zero offset (not local time-zone offset). + +## JS number / .NET TimeSpan +JavaScript lacks a built-in type for representing time spans (at least until the "Temporal" API +is standardized). The common practice is to represent basic time spans as a number of milliseconds. +So a .NET `TimeSpan` is marshalled to or from a simple JS `number` value. + +Note the `Date.offset` property introduced above is intentionally NOT a millisecond timespan value. +It is a whole (positive or negative) number of minutes, because `DateTimeOffset` does not support +second or millisecond precision for offsets. diff --git a/docs/reference/delegates.md b/docs/reference/delegates.md new file mode 100644 index 00000000..fb85a4f7 --- /dev/null +++ b/docs/reference/delegates.md @@ -0,0 +1,27 @@ +# Delegates + +An exported .NET delegate type is converted to a TypeScript function type definition: +```C# +[JSExport] +public delegate string ExampleCallback(int arg1, bool arg2); +``` +```TS +export function ExampleCallback(arg1: number, arg2: boolean): string; +``` + +Then a JavaScript function can be passed to a .NET API that expects a delegate of that type, and +the parameters and return value will be marshalled accordingly. + +```C# +[JSExport] +public static class Example +{ + public static void RegisterCallback(ExampleCallback cb) { … } +} +``` +```JS +Example.registerCallback((arg1, arg2) => 'ok'); +``` + +This is one way for .NET to call back into JavaScript. Another way is to +[implement a .NET interface with a JavaScript class](./classes-interfaces#implement-a-net-interface-with-a-js-class). diff --git a/docs/reference/enums.md b/docs/reference/enums.md new file mode 100644 index 00000000..aa7fd11b --- /dev/null +++ b/docs/reference/enums.md @@ -0,0 +1,21 @@ +# Enums + +.NET `enum` types are marshalled as TypeScript-style +[numeric enums](https://www.typescriptlang.org/docs/handbook/enums.html#numeric-enums) +including [reverse mappings](https://www.typescriptlang.org/docs/handbook/enums.html#numeric-enums) +of enum member values to names. + +```C# +[JSExport] +public enum ExampleEnum +{ + A = 1, +} +``` + +```JS +const a = ExampleEnum.A; // 1 +const nameOfA = ExampleEnum[a]; // 'A' +``` + +(Enum members are not auto-camel-cased by the marshaller.) diff --git a/docs/reference/events.md b/docs/reference/events.md new file mode 100644 index 00000000..5f5eae76 --- /dev/null +++ b/docs/reference/events.md @@ -0,0 +1,7 @@ +# Events + +.NET Events are [not yet supported](https://github.com/microsoft/node-api-dotnet/issues/59). + +It is possible to work with JS events from .NET by calling the JS `addEventListener()` or similar +method and passing a [`JSFunction`](./dotnet/Microsoft.JavaScript.NodeApi/JSFunction) callback. +But there is no automatic marshalling yet to project a JS event as a .NET event. diff --git a/docs/reference/exceptions.md b/docs/reference/exceptions.md new file mode 100644 index 00000000..9a15722b --- /dev/null +++ b/docs/reference/exceptions.md @@ -0,0 +1,40 @@ +# Exceptions + +## JavaScript calling .NET + +If JavaScript code calls a .NET property or method that throws an exception, the exception +gets re-thrown to JS as a JavaScript `Error`. + +For now, only the base `Error` type is thrown. Eventually the JS marshaller could +[generate different `Error` subclasses for each .NET `Exception` subclass](https://github.com/microsoft/node-api-dotnet/issues/205). + +To explicitly throw a specific type of error from .NET to JavaScript, use the one of the `Throw*` +methods of the [`JSError`](./dotnet/Microsoft.JavaScript.NodeApi/JSError) class. + +## .NET calling JavaScript + +If .NET code calls a JS function that throws an error, the error gets re-thrown to .NET as a +[`JSException`](./dotnet/Microsoft.JavaScript.NodeApi/JSException). The +[`JSException.Error`](./dotnet/Microsoft.JavaScript.NodeApi/JSException/Error) property provides +access to the error value that was thrown from JavaScript. + +## Combined stack traces + +When exceptions/errors thrown in .NET or JS are propagated across the boundary between runtimes, +their stack traces are automatically combined. + +In this example, JavaScript code calls a .NET method that throws an exception: + +``` +Error: Test error thrown by .NET. + at Microsoft.JavaScript.NodeApi.TestCases.Errors.ThrowDotnetError(String message) in D:\node-api-dotnet\test\TestCases\Errors.cs:line 13 + at Microsoft.JavaScript.NodeApi.Generated.Module.Errors_ThrowDotnetError(JSCallbackArgs __args) in napi-dotnet.NodeApi.g.cs:line 357 + at Microsoft.JavaScript.NodeApi.JSNativeApi.InvokeCallback[TDescriptor](napi_env env, napi_callback_info callbackInfo, JSValueScopeType scopeType, Func`2 getCallbackDescriptor) in JSNativeApi.cs:line 1070 + at catchDotnetError (D:\node-api-dotnet\test\TestCases\errors.js:14:12) + at Object. (D:\node-api-dotnet\test\TestCases\errors.js:41:1) +``` + - Frame 5: ThrowDotnetError() - C# method that threw the exception + - Frame 4: Errors_ThrowDotnetError() - Marshalling code (auto-generated) + - Frame 3: InvokeCallback() - JS to .NET transition + - Frame 2: catchDotnetError() - JS code that called the .NET method and caught its error + - Frame 0: Top-level JS statement that called catchDotnetError() diff --git a/docs/reference/extension-methods.md b/docs/reference/extension-methods.md new file mode 100644 index 00000000..cd0885e7 --- /dev/null +++ b/docs/reference/extension-methods.md @@ -0,0 +1,31 @@ +# .NET Extension Methods in JavaScript + +Extension methods are important to the usability and discoverability of many .NET libraries, yet +JavaScript has no built-in support for extension methods. Since the JavaScript type system is +very dynamic, the JS marshaller can simulate extension methods by dynamically attaching the methods +to the types they apply to. + +Extension methods are supported only in the +[dynamic invocation scenario](../scenarios/js-dotnet-dynamic) +since pre-built .NET APIs were most likely not designed with JavaScript in mind. But when developing +a [Node.js addon module in C#](../scenarios/js-dotnet-module) the expectation is that the APIs +specifically exported to JavaScript with `[JSExport]` should be designed without any need for +extension methods. + +Extension methods may be provided by the same assembly as the target type, or a different assembly. +When provided by a different assembly, it may be necessary to explicitly import the other assembly, +otherwise the extension method will not be registered on the target type. + +Here is a snippet from the Semantic Kernel example. The Semantic Kernel library makes heavy use of +extension methods. + +```JS +import dotnet from 'node-api-dotnet'; +import './bin/Microsoft.SemanticKernel.Core.js'; +import './bin/Microsoft.SemanticKernel.Connectors.OpenAI.js'; + +const kernelBuilder = dotnet.Microsoft.SemanticKernel.Kernel.CreateBuilder(); + +// Call an extension method provided by the MS.SK.Connectors.OpenAI assembly. +kernelBuilder.AddAzureOpenAIChatCompletion(deployment, endpoint, key); +``` diff --git a/docs/reference/generics.md b/docs/reference/generics.md new file mode 100644 index 00000000..4d1710f4 --- /dev/null +++ b/docs/reference/generics.md @@ -0,0 +1,62 @@ +# .NET Generics in JavaScript + +The JavaScript runtime type system lacks generics, so .NET generic types and methods are projected +into JavaScript using a special convention. The projections are suffixed with the dollar (`$`) +character, chosen because it is a valid identifier charater in JavaScript but not in C#, and +because it looks like an operator. + +Note while TypeScript does have generics, they are merely a compile-time facade and do not have +any effect on runtime binding or execution. So it is unfortunately not possible in most cases +to directly map .NET generics to TypeScript generics. (The exception is when [mapping .NET generic +collection interfaces to TypeScript generic collections](./arrays-collections#collections) like +`Array` and `Map`, which does work.) + +```JavaScript +// JavaScript +import dotnet from 'node-api-dotnet'; +const System = dotnet.System; +System.Enum.Parse$(System.DayOfWeek)('Tuesday'); // Call generic method +System.Comparer$(System.DateTime).Create() // Call static method on generic class +const TaskCompletionSourceOfDate = System.TaskCompletionSource$(System.DateTime); +new TaskCompletionSourceOfDate(); // Create instance of generic class +new System.TaskCompletionSource(); // Create instance of non-generic class +``` + +A .NET _generic method definition_ is projected as a function with a `$` suffix. That function +takes generic type parameter(s) and returns the _specialized_ generic function. So, .NET +`Enum.Parse()` is projected as `Enum.Parse$(Type)`. + +A .NET _generic type definition_ is also projected as a function with a `$` suffix. That function +takes generic type parameter(s) and returns the _specialized_ generic type. So, .NET `Comparer` +is projected as `Comparer$(Type)`. + +If a type has both generic and non-generic variants, the non-generic type is still available +normally, without any `$` suffix. If a type has multiple generic variants then the one `$` function +returns the requested type specialization according to the number of type arguments supplied. + +## Getting a type full name +Calling the `toString()` method on the JS projection of any generic type definition, specialized +type, or non-generic type returns the full .NET type name. This may be helpful for diagnostics. + +```JavaScript +// JavaScript +System.Comparer$.toString(); // 'System.Comparer' +System.Comparer$(System.DateTime).toString(); // 'System.Comparer +System.String.toString(); // 'System.String' +``` + +## Static binding / AOT +The above applies to [dynamic binding](../scenarios/js-dotnet-dynamic), when the `node-api-dotnet` +library can use reflection to locate generic type and method definitions, specialize them, and +invoke them. But some of that is impossible to do in an +[ahead-of-time compiled environment](../features/dotnet-native-aot). Dynamically specifying generic +type arguments from JavaScript would require reflection and code-generation, whch are not supported +in an AOT executable. (In a pure C# application, the AOT compiler would be able to know exactly +what type arguments are used with any generic type or method, so it can generate specialized code +accordingly.) + +This means that [C# Node API modules compiled as AOT](../scenarios/js-aot-module) cannot export +generic types or methods. They can still use generic types in properties or methods, when the type +argument is specified ahead of time. For example it is OK to use AOT to export a method that has a +parameter of type `KeyValuePair`. But exporting a generic method with type parameter +`T` and method parameter `KeyValuePair` will not work with AOT. diff --git a/docs/reference/js-apis.md b/docs/reference/js-apis.md new file mode 100644 index 00000000..6bd8bb5c --- /dev/null +++ b/docs/reference/js-apis.md @@ -0,0 +1,49 @@ +--- +outline: deep +--- + +# Runtime API Examples + +This page demonstrates usage of some of the runtime APIs provided by VitePress. + +The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: + +```md + + +## Results + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+``` + + + +## Results + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+ +## More + +Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/reference/js-dotnet-types.md b/docs/reference/js-dotnet-types.md new file mode 100644 index 00000000..a67d00e3 --- /dev/null +++ b/docs/reference/js-dotnet-types.md @@ -0,0 +1,30 @@ +# JavaScript / .NET Type Mappings + +This table provides a quick reference for how different .NET and JavaScript runtime types and +concepts are handled by the JS marshaller and type-definitions generator. Follow the links for more +details about each topic. + +| Topic | Summary| +|---------------------------------|--------| +| [Basic types](./basic-types) | `string` => `string`
`bool` => `boolean`
`byte`, `short`, `int`, `long`, `float`, `double` => `number` +| [Null & undefined](./null-undefined) | .NET `null` => JS `undefined`
JS `null` or `undefined` => .NET `null` +| [Classes & interfaces](./classes-interfaces) | .NET classes can be constructed and used in JS. Class or interface instances are marshalled by reference. JS code can implement .NET interfaces. +| [Structs & tuples](./structs-tuples) | .NET structs can be constructed and used in JS. Struct instances and tuples are marshalled by value.
.NET `Tuple` or `ValueTuple` => JS `[ A, B ]` (array tuple) +| [Enums](./enums) | .NET enums are projected as TS non-const enums including [reverse mappings](https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings). +| [Arrays & collections](./arrays-collections) | .NET `T[]` or `IList` => JS `T[]`
.NET `IDictionary` => JS `Map`
.NET `IEnumerable` => JS `Iterable`
.NET `Memory` => JS `Uint8Array` +| [Delegates](./delegates) | .NET `Func` => JS Function `(TValue) => TRet` +| [Streams](./streams) | .NET `Stream` => Node.js `Duplex` +| [Dates & times](./dates) | .NET `DateTime` => JS `Date`
.NET `DateTimeOffset` => JS `Date`
.NET `TimeSpan` => JS `number` (milliseconds) +| [Other special types](./other-types) | .NET `Guid` => JS `string`
.NET `BigInteger` => JS `bigint` +| [Async & promises](./async-promises) |.NET `Task` => JS `Promise` +| [Ref & out params](./ref-out-params) | .NET `ref` and `out` params are returned via a result object:
C# `bool F(ref string a, out int b)` =>
JS `f(a: string) => { a: string, b: int, result: boolean }` +| [Generics](./generics)* | .NET generics are supported in JS, with special `$` syntax and some limitations. +| [Extension methods](./extension-methods)* | .NET extension methods are supported in JS. +| [Overloaded methods](./overloaded-methods) | .NET overloaded methods can be called from JS, though overload resolution has some limitations. +| [Events](./events) | .NET events are [not yet implemented](https://github.com/microsoft/node-api-dotnet/issues/59). +| Fields | .NET `class` or `struct` fields are [not yet implemented](https://github.com/microsoft/node-api-dotnet/issues/63). Use properties instead. +| [Exceptions](./exceptions) | .NET `Exception` is thrown as JS `Error`, with combined stack trace. +| [Namespaces](./namespaces)* | .NET namespaces are preserved on the `node-api-dotnet` module object:
`import dotnet from 'node-api-dotnet';`
`dotnet.System.Console.WriteLine()` + +\* These are only supported with the [dynamic invocation scenario](../scenarios/js-dotnet-dynamic). + diff --git a/docs/reference/msbuild-props.md b/docs/reference/msbuild-props.md new file mode 100644 index 00000000..14bd15d0 --- /dev/null +++ b/docs/reference/msbuild-props.md @@ -0,0 +1,15 @@ +# MSBuild Properties + +The following properties can be used to customize the build processes for generating and packaging .NET projects for use from JavaScript. + +| Property Name | Description | +|-----------------------------------------------|-------------| +| `GenerateNodeApiTypeDefinitions` | Set to `true` to generate TypeScript type definitions for .NET APIs in the current project. (This is enabled by default when referencing the `Microsoft.JavaScript.NodeApi.Generator` package.) See [Develop a Node.js addon module](../scenarios/js-dotnet-module). | +| `GenerateNodeApiTypeDefinitionsForReferences` | Set to `true` to generate TypeScript type definitions for .NET APIs in assemblies referenced by the current project. (This is enabled by default when **_an empty project_** references the `Microsoft.JavaScript.NodeApi.Generator` package.) See [Dynamically invoke .NET APIs from JavaScript](../scenarios/js-dotnet-dynamic). | +| `NodeApiTypeDefinitionsFileName` | Name of the type-definitions file generated for a project. Defaults to `$(TargetName).d.ts`. | +| `NodeApiJSModuleType` | Set to either `commonjs` or `esm` to specify the module system used by the generated type definitions. If unspecified, the module type is detected automatically from `package.json`, which is usually correct. | +| `PublishNodeModule` | Set to `true` to produce a Native AOT `.node` binary and `.js` module-loader script when building the `Publish` target. The files will be placed in the directory indicated by the `PublishDir` variable. See [Develop a Node.js addon module with .NET Native AOT](../scenarios/js-aot-module). | +| `PublishMultiPlatformNodeModule` | If `true`, the published `.node` binary file will be placed in a sub-directory according to the targeted [`RuntimeIdentifier`](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#runtimeidentifier), for example `win-x64`. Then the publish process may be run spearately for multiple runtime-identifiers, and the module-loader script chooses the approrpriate one at runtime. | +| `PackNpmPackage` | Set to `true` to run `npm pack` when building the `Publish` target. Requires a `package.json` file in the project directory. The npm package will be placed in the directory indicated by the `PackageOutputPath` variable. (Note this does not use the MSBuild "Pack" target because "Pack" runs before "Publish", while npm packaging must use the Native AOT binaries produced by the "Publish".) | +| `EmitCompilerGeneratedFiles` | If `true` (the default), the generated C# code (`*.g.cs`) for a node module project will be emitted under `$(IntermediateOutputPath)/generated`. This can be helpful for debugging the generated code for marshalling .NET APIs to/from JS. | + diff --git a/docs/reference/namespaces.md b/docs/reference/namespaces.md new file mode 100644 index 00000000..f4aeb835 --- /dev/null +++ b/docs/reference/namespaces.md @@ -0,0 +1,41 @@ +# Namespaces + +.NET namespaces are projected to JavaScript only in the +[dynamic invocation scenario](../scenarios/js-dotnet-dynamic) +since pre-built .NET APIs were designed with namespaces in mind. + +But when developing a [Node.js addon module in C#](../scenarios/js-dotnet-module) the APIs +specifically exported to JavaScript with `[JSExport]` are exposed on the module object without +any additional namespacing. (In that scenario, any .NET namespaces have no impact on the exported +JavaScript API.) + +## Importing assemblies and namespaces + +When dynamically importing .NET assemblies, all namespaces and types provided by each imported +assembly are merged onto the top-level `dotnet` object (created by the `node-api-dotnet` package). + +In this snippet from the Semantic Kernel example, JavaScript code imports three .NET SemanticKernel +assemblies. Note the result of the `import` statement is not named or assigned, because these +imports do not return _modules_, rather the imports cause a side-effect of merging all of the +types into the combined .NET namespace hierarchy. + +```JS +import dotnet from 'node-api-dotnet'; +import './bin/Microsoft.SemanticKernel.Abstractions.js'; +import './bin/Microsoft.SemanticKernel.Core.js'; +import './bin/Microsoft.SemanticKernel.Connectors.OpenAI.js'; + +// All of the namespaces, types, and extension methods from the 3 imported +// assemblies are now available on the `dotnet` object. +const kernelBuilder = dotnet.Microsoft.SemanticKernel.Kernel.CreateBuilder(); +``` + +The import mechanism is designed this way because when working with .NET APIs there is an +expectation that all .NET types are in a single combined namespace hierarchy. Since each .NET +assembly can provide types to multiple namespaces, and multiple assemblies can provide types to +each namespace, .NET developers are typically not aware of precisely which assembly provides +every type or extension method. (Even if you think you know, you might be mistaken because types +can be [forwarded](https://learn.microsoft.com/en-us/dotnet/standard/assembly/type-forwarding) to +other assemblies.) This is different from typical JavaScript development, where APIs are explicitly +imported from specific JS modules or packages (though JS packages can forward APIs from other +modules as well). diff --git a/docs/reference/null-undefined.md b/docs/reference/null-undefined.md new file mode 100644 index 00000000..9bbc6e4b --- /dev/null +++ b/docs/reference/null-undefined.md @@ -0,0 +1,218 @@ +# Null and Undefined + +| C# Type | JS Type | +|----------|-------------| +| `null` | `undefined` | +| `null` | `null` | + +In summary, a .NET `null` values is mapped to the JS `undefined` value (not JS `null`), while both +JS `undefined` and `null` map to .NET `null`. The rest of this page explains the reasoning behind +the design choice. + +Consider: + - JavaScript has both `null` and `undefined` primitive values, while .NET has + only `null`. + - A default/uninitialized value in JS is `undefined`, while in .NET the default + is `null` (for reference types and `Nullable` value types). + - In JavaScript, `typeof undefined === 'undefined'` and `typeof null === 'object'`. + +So how should these inconsistencies in type systems of the two platforms be +reconciled during automatic marshalling? + +> Note: Instead of relying on automatic marshalling, .NET code has the option to +[work directly with `JSValue`](../features/js-types-in-dotnet) and its distinct `JSValue.Null` and +`JSValue.Undefined` values. + +## Marshalling .NET `null` to JavaScript +For discussion here, `T` may any _specific_ (not `object/any`) type, including both +marshal-by-value (number, struct, enum) and marshal-by-ref (string, class, interface) +types. + +If `T` is not nullable (neither a `Nullable` value type nor a nullable reference +type), then the TypeScript projection will allow neither `null` nor `undefined`. +However, **_marshalling must still handle null .NET reference values even when the +type is non-nullable_**. + +In any case a JS `undefined` value passed to .NET is always converted to `null` by +the marshaller. + +### Option A: .NET `null` -> JS `undefined` +If .NET `null` values are marshalled as JS `undefined`, that has the following effects: + +| Description | C# API | JS API | JS Notes | +|-------------|--------|--------|----------| +| Method with optional param | `void Method(T? p = null)` | `method(p?: T): void` | `p` is `undefined` in JS if a .NET caller omitted the parameter _or_ supplied `null`;
`p` is never `null` in JS when called by .NET. +| Method with nullable param | `void Method(T? p)` | `method(p: T \| undefined): void` | `p` is `undefined` in JS if a .NET caller supplied `null`. +| Method with nullable return | `T? Method()` | `method(): T \| undefined` | Result is `undefined` in JS if .NET method returned `null`;
result is never `null` when returned by .NET. +| Nullable property | `T? Property` | `property?: T` | Property value is `undefined` (_but the property exists_) in JS if the object was passed from .NET;
value is never `null` (or missing) on an object from .NET. + +### Option B: .NET `null` -> JS `null` +Alternatively, if .NET `null` values are marshalled as JS `null`, that has the following effects: + +| Description | C# API | JS API | JS Notes | +|-------------|--------|--------|----------| +| Method with optional param | `void Method(T? p = null)` | `method(p: T \| null): void` | `p` is `null` in JS if a .NET caller omitted the parameter _or_ supplied `null`;
`p` is never `undefined` in JS when called by .NET. +| Method with nullable param | `void Method(T? p)` | `method(p?: T \| null): void` | `p` is `null` in JS if a .NET caller supplied `null`. +| Method with nullable return | `T? Method()` | `method(): T \| null` | Result is `null` in JS if .NET method returned `null`;
result is never `undefined` when returned by .NET. +| Nullable property | `T? Property` | `property: T \| null` | Property value is `null` (and the property exists) in JS if the object was passed from .NET;
value is never `undefined` (or missing) on an object from .NET. + +## JavaScript `null` vs `undefined` practices +While `null` and `undefined` are often used interchangeably, the distinction can sometimes +be important. Let's analyze some ways in which `null` and/or `undefined` values might be +handled differently, and how common those practices are in JavaScript. + +### Detecting optional parameters to a JavaScript function +In JavaScript, there are several common ways to detect when an optional +parameter was not supplied to a function: +```TS +function exampleFunction(optionalParameter?: any): void +``` +1. Common / best practice: Check if the value type is equal to `'undefined'`.
+ `if (typeof optionalParameter === 'undefined')` + +2. Somewhat common: Check if the value is strictly equal to `undefined`.
+ `if (optionalParameter === undefined)` + +3. Common / best practice in TS & ES2020: Use the "nullish coalescing operator", which + handles both `null` and `undefined`:
+ `value = optionalParameter ?? defaultValue` + +4. Traditional and still common (occasionally error-prone): Check if the value is falsy.
+ `if (!optionalParameter)`
+ `value = optionalParameter || defaultValue` + +5. Less common: Check the length of the `arguments` object. (Use of the special + `arguments` object is [discouraged]( + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) + in modern JS, in favor of rest parameters.)
+ `if (arguments.length === 0)` + +6. Uncommon: Check if the value is null with _loose_ equality. +It handles both `null` and `undefined` because `null == undefined`. (The +loose equality operator is usually flagged by linters.)
+ `if (optionalParameter == null)` + +| |A: null->undefined|B: null->null| +|-|:----------------:|:-----------:| +|1|✅|❌| +|2|✅|❌| +|3|✅|✅| +|4|✅|✅| +|5|❌|❌| +|6|✅|✅| + +### Checking the return value of a function/method +A JavaScript function my return `undefined`, or `null`, when it yields no result. +There is [no strong consensus among the JS developer community]( +https://stackoverflow.com/questions/37980559/is-it-better-to-return-undefined-or-null-from-a-javascript-function) +about when to use either one; some developers may prefer one or the other while +others may not think very hard about the distinction. There are a few ways the +caller might check the return value: +```TS +function exampleFunction(): any +``` +1. Traditional and still common (occasionally error-prone): Check if the result value is + falsy.
+ `if (!result)` +2. Common: Check if the result value type is `'undefined'` or value is strictly equal + to `undefined`.
+ `if (typeof result === 'undefined')`
+ `if (result === undefined)` +3. Uncommon: Check if the result value is null with _loose_ equality.
+ `if (result == null)` + +| |A: null->undefined|B: null->null| +|-|:----------------:|:-----------:| +|1|✅|✅| +|2|✅|❌| +|3|✅|✅| + +### Detecting optional properties on a JavaScript object +In JavaScript, there are a few ways to detect when an optional property was +not supplied with an object: +```TS +interface Example { + optionalProperty?: any; +} +``` +1. Common: Use the `in` operator.
`if ('optionalProperty' in exampleObject)` +2. Common: Use `hasOwnProperty` or the more modern `hasOwn` replacement.
+ `if (!exampleObject.hasOwnProperty('optionalProperty'))`
+ `if (!Object.hasOwn(exampleObject, 'optionalProperty'))` +3. Traditional and still common (occasionally error-prone): Check if the property value + is falsy.
+ `if (!exampleObject.optionalProperty)`
+ `if (!exampleObject['optionalProperty'])`
+ `value = exampleObject.optionalProperty || defaultValue` +4. Less common: Check if the property value type is `'undefined'` or value is + strictly equal to `undefined`.
+ `if (typeof exampleObject.optionalProperty === 'undefined')`
+ `if (exampleObject.optionalProperty === undefined)` +5. Less common: Use the nullish coalescing operator
+ `value = exampleObject.optionalProperty ?? defaultValue` +6. Uncommon: Check if the property value is null with _loose_ equality.
+ `if (exampleObject.optionalProperty == null)` + +| |A: null->undefined|B: null->null| +|-|:----------------:|:-----------:| +|1|❌|❌| +|2|❌|❌| +|3|✅|✅| +|4|✅|❌| +|5|✅|✅| +|6|✅|✅| + +Note even when marshalling `null` to `undefined`, common checks that rely on the +_existince_ of properties can fail. And operations that enumerate the object properties +may have differing behavior for missing properties vs ones with `undefined` value. For +more on that subtle distinction, see [TypeScript's `--exactOptionalPropertyTypes` option]( +https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-beta/#exact-optional-property-types). + +### Checking for strict `null` equality +JavaScript code _can_ specifically check if a parameter, return value, or +property value is strictly equal to null: +```JS +if (value === null) +``` +|A: null->undefined|B: null->null| +|:----------------:|:-----------:| +|❌|✅| + +More experienced JavaScript developers never write such code, since +they are aware of the pervasiveness of `undefined`. But it can be easy +for developers coming from other languages (like C# or Java) to write +such code while assuming `null` works the same way, or merely from +muscle memory. + +### JS APIs with semantic differences between `undefined` vs `null` +A JavaScript API _could_ assign wholly different meanings to the two values, for +instance using `undefined` to represent an uninitialized state and `null` to +represent an intialized-but-cleared state. Since automatic marshalling of .NET +`null` cannot support that distinction, calling such a JS API from .NET would +require direct use of `JSValue.Undefined` and `JSValue.Null` (or perhaps a +JS wrapper for the targeted API) to handle the disambiguation. But such an API +design aspect would likely confuse many JavaScript developers as well, so it +is not a common occurrence. + +## Design Choice +In the tables above, there are fewer ❌ marks in column A; this indicates +that mapping .NET `null` to JS `undefined` is the better choice for default +marshalling behavior. + +There are a few rare cases in which the default may be problematic: +1. Omitted optional function parameters, when the JS function body checks +`arguments.length`. +2. Omitted optional properties of an object, when the JS code checks +whether the object has the property, or enumerates the object properties. +3. A nullable (not optional) value where the JS code checks for strict +null equality. + +To handle these cases (and any other situations that might arise), we can add +flags to the ([planned](https://github.com/microsoft/node-api-dotnet/issues/64)) +`[JSMarshalAs]` attribute to enable setting the null-value marshalling behavior +of a specific .NET method parameter, return value, or property to one of three +options: + - `undefined` (default) + - `null` + - omit - Exclude from the function arguments (if there are no non-omitted + arguments after it), or exclude from the properties of the marshalled object. diff --git a/docs/reference/other-types.md b/docs/reference/other-types.md new file mode 100644 index 00000000..895b3c9c --- /dev/null +++ b/docs/reference/other-types.md @@ -0,0 +1,24 @@ +# Other Special Types + +## BigInteger + +| C# Type | JS Type | +|--------------|----------| +| `BigInteger` | `BigInt` | + +.NET [`BigInteger`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.biginteger) +converts to and from JavaScript +[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) +with no loss in precision. (Conversion internally allocates and copies memory for the number data.) +The [`JSBigInt`](./dotnet/Microsoft.JavaScript.NodeApi/JSBigInt) class supports working directly +with JS `BigInt` values, and converting to/from .NET `BigInteger`. + +## Guid + +| C# Type | JS Type | +|---------|----------| +| `Guid` | `string` | + +A .NET `Guid` is marshalled to JS as a `string` in the default `"D"` format. Marshalling in the +other direction supports any format accepted by +[`Guid.Parse()`](https://learn.microsoft.com/en-us/dotnet/api/system.guid.parse). diff --git a/docs/reference/overloaded-methods.md b/docs/reference/overloaded-methods.md new file mode 100644 index 00000000..09a4dc2a --- /dev/null +++ b/docs/reference/overloaded-methods.md @@ -0,0 +1,29 @@ +# Overloaded Methods + +.NET APIs commonly include multiple overloads for a method. JavaScript does not directly support +"overloaded" methods, though it is common practice to implement overload-like behavior in a JS +function by dynamically checking for different argument counts and/or types. + +## Overload resolution + +The JS [marshaller](../features/js-dotnet-marshalling) has limited support for overload +resolution. It can examine the count and types of arguments provided by the JavaScript caller and +select the best matching .NET overload accordingly. + +```C# +[JSExport] +public class OverloadsExample +{ + public static void AddValue(string stringValue); + public static void AddValue(double numberValue); +} +``` +```JS +OverloadsExample.addValue('test'); // Calls AddValue(string) +OverloadsExample.addValue(77); // Calls AddValue(double) +``` + +Currently the overload resolution is limited to examining the JavaScript type of each argument +(`string`, `number`, `object`, etc), but that is not sufficient to select between overloads that +differ only in the _type of object_. +[More advanced overload resolution is planned.](https://github.com/microsoft/node-api-dotnet/issues/134) diff --git a/docs/reference/packages-releases.md b/docs/reference/packages-releases.md new file mode 100644 index 00000000..d15dcf20 --- /dev/null +++ b/docs/reference/packages-releases.md @@ -0,0 +1,33 @@ +# Packages & Releases + +## NuGet packages + +[`Microsoft.JavaScript.NodeApi`](https://www.nuget.org/packages/Microsoft.JavaScript.NodeApi/) - +Contains the core functionality for interop between .NET and JavaScript, including runtime +code-generation for dynamic marshalling. See the [.NET API reference](../reference/dotnet/). + +[`Microsoft.JavaScript.NodeApi.Generator`](https://www.nuget.org/packages/Microsoft.JavaScript.NodeApi.Generator/) - +Contains the MSBuild tasks and supporting assemblies for generating marshalling code at compile +time, and for generating TypeScript type defintions for .NET APIs. + +## NPM packages + +[`node-api-dotnet`](https://www.npmjs.com/package/node-api-dotnet) - Supports loading .NET +assemblies into a Node.js process. Contains the native .NET hosting modules +(`Microsoft.JavaScript.NodeApi.node`, built with .NET Native AOT) for all supported platforms, +the runtime assemblies (`Microsoft.JavaScript.NodeApi.dll`) for all supported target frameworks, +and loader scripts. See the [JavaScript API reference](../reference/js/). + +[`node-api-dotnet-generator`](https://www.npmjs.com/package/node-api-dotnet-generator) - A Node.js +command-line tool that is a wrapper around the .NET generator assembly. It enables generating +TypeScript type definitions without using MSBuild. See the CLI reference at +[TypeScript Type Definitions](../features/type-definitions). + +## Releases + +Packages are published to nuget and npm automatically, usually around an hour after any PR +is merged. + +While the project is in pre-release phase, there may be occasional breaking API changes between +versions less than 1.0. Starting with v1.0 (expected late 2024), it will follow +[semantic versioning practices](https://semver.org/). diff --git a/docs/reference/ref-out-params.md b/docs/reference/ref-out-params.md new file mode 100644 index 00000000..cae0dfc4 --- /dev/null +++ b/docs/reference/ref-out-params.md @@ -0,0 +1,90 @@ +# Ref and Out Parameters + +JavaScript does not have any concept of `ref` or `out` parameters like C#. So the JS marshaller +and type-definitions generator apply a transformation to any .NET method that has `ref` or `out` +parameters. + +## The "Try" pattern + +If the .NET method follows the "Try" pattern (the method name starts with `"Try"`, returns `bool`, +and the last parameter is `out`), then in the JS method the `out` parameter is moved to the return +value and a `false` result becomes `undefined`. + +```C# +public class DateTime +{ + public static bool TryParse(string? input, out DateTime result); +} +``` + +```TS +// Generated type definition +export class TimeSpan { + static TryParse(input: string | undefined): number | undefined; +} +``` + +```JS +const timeSpan = dotnet.System.TimeSpan.TryParse(timeSpanString); +if (typeof timeSpan === 'undefined') { + // parse failed +} +``` + +## Method with `out` parameters + +If the .NET method has one or more `out` parameters but does not match the "Try" pattern above, +then the `out` parameters are omitted from the JS method and the return type is an object +with a `result` property for the return value (if the return type is not `void`) and additional +properties for each named `out` parameter. + +```C# +[JSExport] +public static double GetAverage( + IEnumerable data, + out double standardDeviation); +``` + +```TS +// Generated type definition +export function getAverage(data: Iterable) + : { result: number, standardDeviation: number }; +``` + +```JS +const { result, standardDeviation } = getAverage(data); +``` + +## Method with `ref` parameters + +If the .NET method has one or more `ref` parameters, then the `ref` paramaters are included both in +the JS method parameters and the result object. Note it may be necessary to explicitly assign the +output paramter value back to the original variable in order to achieve `ref` semantics. + +```C# +[JSExport] +public static Memory? GetNextToken(Memory input, ref int position); +``` + +```TS +// Generated type definition +export function getNextToken(input: UInt8Array, position: number) + : { result: UInt8Array | undefined, position: number }; +``` + +```JS +const data = new UInt8Array(buffer, offset, length); +let nextPosition = 0; +while (true) { + const { result, position } = getNextToken(data, nextPosition); + if (!result) break; + // (process next token result) + nextPosition = position; // Assign the `ref` output value +} +``` + +If a method has both `ref` and `out` parameters, then the result object includes both the `ref` and +`out` parameters. + +If one of the parameters is named `result`, then the return value property gets the name `_result` +to avoid a name conflict. diff --git a/docs/reference/streams.md b/docs/reference/streams.md new file mode 100644 index 00000000..9f2039f0 --- /dev/null +++ b/docs/reference/streams.md @@ -0,0 +1,32 @@ +# Streams + +| C# Type | JS Type | +|-----------------------|------------| +| `Stream` (read/write) | `Duplex` | +| `Stream` (read-only) | `Readable` | +| `Stream` (write-only) | `Writable` | + +A .NET `Stream` instance is marshalled to and from Node.js +[`Duplex`](https://nodejs.org/api/stream.html#duplex-and-transform-streams), +[`Readable`](https://nodejs.org/api/stream.html#readable-streams), or +[`Writable`](https://nodejs.org/api/stream.html#writable-streams), +depending whether the stream supports reading and/or writing. JS code can seamlessly read from +or write to streams created by .NET, or .NET code can read from or write to streams created by JS. +Streamed data is transferred using shared memory (without any additional sockets or pipes), so +memory allocation and copying is minimized. + +```C# +[JSExport] +public static class Example +{ + public Stream GetContent() { … } +} +``` + +```JS +const stream = Example.getContent(); +stream.on('data', (chunk) => { console.log(chunk.toString()); }); +``` + +The [`NodeStream`](./dotnet/Microsoft.JavaScript.NodeApi.Interop/NodeStream) class provides the +.NET `Stream` adapter over a Node.js `Duplex`, `Readable`, or `Writable` stream. diff --git a/docs/reference/structs-tuples.md b/docs/reference/structs-tuples.md new file mode 100644 index 00000000..94ec9bea --- /dev/null +++ b/docs/reference/structs-tuples.md @@ -0,0 +1,48 @@ +# Structs and Tuples + +## Structs + +.NET structs are projected to TypeScript as classes, however the JS marshaller handles them very +differently from [.NET classes](./classes-interfaces#marshalling-net-classes-to-js). .NET structs +are _marshalled by value_. This has a few implications: + - Every time a .NET `struct` instance is marshalled to JS, a new instance of the corresponding JS + `class` is constructed, with copied/marshalled property values. + - Every time a JS `class` instance is marshalled to a .NET `struct`, a new instance of the `struct` + is constructed, with copied/marshalled property values. + - There is no [object lifetime](../features/js-dotnet-marshalling#object-lifetimes) relationship + between a .NET struct instance and a JS class instance created from it. + - Getting or setting an instance property on a JS `class` instance created from a .NET `struct` + _does not_ invoke the .NET getter / setter code (if any). + - Invoking an instance method on a JS `class` instance created from a .NET `struct` _does_ invoke + the .NET instance method, though the `struct` gets entirely copied/marshalled from JS to .NET + before every .NET method invocation. + +Static properties and methods on a .NET `struct` work the same as on a `class`, since static members +do not deal with an instance that gets marshalled by value. (The `struct` _type_ object is still +marshalled by reference.) + +### Read-only structs and properties + +.NET `struct` types that are `readonly`, or read-only properties of non-`readonly structs` are +[not yet implemented](https://github.com/microsoft/node-api-dotnet/issues/132). + +## Tuples + +| C# Type | JS Type | +|--------------------------------------------|-------------| +| `Tuple`
`ValueTuple` | `[]` | +| `Tuple`
`ValueTuple
` | `[A]` | +| `Tuple`
`ValueTuple` | `[A, B, …]` | + +.NET tuples, including all variations of `Tuple<>` and `ValueTuple<>`, are marshalled as JS arrays, +and the tuple types are projected as TypeScript +[array tuples](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types). +(Tuple field names, if any, are not used in JS.) + +```C# +[JSExport] +public static (string Name, double Value) GetNameAndValue(); +``` +```TS +export function getNameAndValue(): [string, number]; +``` diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 00000000..0afe962e --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,26 @@ +# Requirements + +## Runtime requirements +#### OS + - Windows: x64, arm64 + - Mac: x64, arm64 + - Linux: x64 ([arm64 coming soon](https://github.com/microsoft/node-api-dotnet/issues/80)) + +#### .NET + - For .NET runtime-dependent applications, .NET 4.7.2 or later, .NET 6, or .NET 8 runtime + must be installed. + - For .NET Native AOT applications, .NET is not required on the target system. + - On Linux, AOT binaries may depend on optional system packages. See + [Install .NET on Linux](https://learn.microsoft.com/en-us/dotnet/core/install/linux) + and browse to the distro specific dependencies. + +#### Node.js + - Node.js v18 or later + - Other JS runtimes may be supported in the future. + +## Build requirements + + - .NET 8 SDK + - Optional: .NET 6 SDK, if targeting .NET 6 runtime + - Optional: .NET Framework 4.x developer pack, if targeting .NET Framework + - Node.js v18 or later diff --git a/docs/scenarios/dotnet-js.md b/docs/scenarios/dotnet-js.md new file mode 100644 index 00000000..e46b9611 --- /dev/null +++ b/docs/scenarios/dotnet-js.md @@ -0,0 +1,94 @@ +# Embed a JS runtime in a .NET application + +::: warning +This functionality is still experimental. It works, but the process is not as streamlined as it +should be. +::: + +## Building the required `libnode` binary +This project depends on a [PR to Node.js](https://github.com/nodejs/node/pull/43542) that adds +simpler ABI-stable embedding APIs to `libnode`. Until that PR is merged, you'll need to build your +own binary from a branch. And even after it's merged, the Node.js project does not currently and +has no plans to publish official `libnode` binaries. + +1. Install the [prerequisites for building Node.js](https://github.com/nodejs/node/blob/main/BUILDING.md#building-nodejs-on-supported-platforms). +(On Windows this is basically Python 3 and either VS 2022 or the C++ build tools.) + +2. Clone the napi-libnode repo/branch: + +```shell +mkdir libnode +cd libnode +git clone https://github.com/jasongin/nodejs -b napi-libnode-v20.9.0 --single-branch . +``` + +3. Build in shared-library mode: +::: code-group +```shell [Windows] +.\vcbuild.bat x64 release dll openssl-no-asm +``` +```shell [Mac / Linux] +./configure --shared; make -j4 +``` +::: +The build may take an hour or more depending on the speed of your system. + +4. A successful build produces a binary in the `out/Release` directory: + - `libnode.dll` (Windows) + - `libnode.dylib` (Mac) + - `libnode.???.so` (Linux) + +## Importing JS modules into .NET + +1. Load `libnode` and initialize a Node.js "platform" and "environment" instance: +```C# +// Find the path to the libnode binary for the current platform. +string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; +string libnodePath = Path.Combine(baseDir, "libnode.dll"); +NodejsPlatform nodejsPlatform = new(libnodePath); +var nodejs = nodejsPlatform.CreateEnvironment(baseDir); +``` + +There can only be one `NodejsPlatform` instance per process, so typically it would be stored +in a static variable. From the platform, multiple `NodejsEnvironment` instances may be created +and disposed. + +The directory provided to the environment instance is the base for package resolution. Any packages +or modules imported in the next step must be installed (in a `node_modules` directory) in that base +directory or a parent directory. + +2. Invoke a simple script from C#: +```C# +await nodejs.RunAsync(() => +{ + JSValue.RunScript("console.log('Hello from JS!');"); +}); +``` + +Be aware of JavaScript's single-threaded execution model. **All JavaScript operations must be +performed from the JS environment thread**, unless otherwise noted. Use any of the `Run()`, +`RunAsync()`, `Post()`, or `PostAsync()` methods on the JS environment instance to switch to the +JS thread. Also keep in mind any JavaScript values of type `JSValue` (or any of the more specific +JS value struct types) are not valid after switching off the JS thread. A `JSReference` can hold +on to a JS value for future use. See also +[JS Threading and Async Continuations](../features/js-threading-async) and +[JS References](../features/js-references). + +3. Import modules or module properties: +```C# +// Import * from the `fluid-framework` module. Items exported from the module will be +// available as properties on the returned JS object. +JSValue fluidPackage = nodejs.Import("fluid-framework"); + +// Import just the `SharedString` class from the `fluid-framework` module. +JSValue sharedStringClass = nodejs.Import("fluid-framework", "SharedString"); +``` +As explained above, importing modules must be done on the JS thread. + +## Debugging the JavaScript code in a .NET process +```C# +int pid = Process.GetCurrentProcess().Id; +Uri inspectionUri = nodejs.StartInspector(); +Debug.WriteLine($"Node.js ({pid}) inspector listening at {inspectionUri}"); +``` +Then attach a JavaScript debugger such as VS Code or Chrome to the inspection URI. diff --git a/docs/scenarios/index.md b/docs/scenarios/index.md new file mode 100644 index 00000000..40b83e8f --- /dev/null +++ b/docs/scenarios/index.md @@ -0,0 +1,36 @@ +--- +next: + text: Dynamic .NET from JS + link: ./js-dotnet-dynamic +--- + +# JS / .NET Interop Scenarios + +There are four primary scenarios enabled by this project. Choose one of them for getting-started +instructions: + + - [Dynamically invoke .NET APIs from JavaScript](./js-dotnet-dynamic)
+ Dynamic invocation is easy to set up: all you need is the `node-api-dotnet` package and the + path to a .NET assembly you want to call. It is not quite as fast as a C# addon module because + marshalling code must be generated at runtime. It is best suited for simpler projects with + limited interop needs. + + - [Develop a Node.js addon module in C#](./js-dotnet-module)
+ A C# Node module exports specific types and methods from the module to JavaScript. It can be + faster because marshalling code is generated at compile time, and the shape of the APIs + exposed to JavaScript can be designed or adapted with JS interop in mind. It is best suited + for more complex projects with advanced or high-performance interop requirements. + + - [Develop a Node.js addon module in C# with .NET Native AOT](./js-aot-module)
+ A variation on the previous scenario, this is best suited for creation of a re-usable Node.js + native addon that loads quickly and does not depend the .NET runtime. However binaries are + platform-specific, so packaging and distribution is more difficult. + + - [Embed a JS runtime in a .NET application](./dotnet-js)
+ Run Node.js (or another JS runtime) in a .NET application process, import `npm` packages + and/or JavaScript module files and call them from .NET. + +All of these scenarios support +[auto-generated TypeScript type definitions](../features/type-definitions), +[automatic efficient marshalling](../features/js-dotnet-marshalling), +and [error propagation](../reference/exceptions). diff --git a/docs/scenarios/js-aot-module.md b/docs/scenarios/js-aot-module.md new file mode 100644 index 00000000..fb238186 --- /dev/null +++ b/docs/scenarios/js-aot-module.md @@ -0,0 +1,46 @@ +# Develop a Node.js addon module in C# with .NET Native AOT + +### About .NET Native AOT +AOT compiled modules load more quickly and _do not have any runtime dependency .NET_. However, +.NET Native AOT has some limitations, so you should understand the implications before starting +on this path. Some of the considerations include: + - .NET 8 SDK or later is required at build time. (Not at run time.) + - AOT binaries are much larger: at least 4-10 MB depending on the platform. + - AOT code can only call other native code. That may include other .NET Native AOT assemblies, + but NOT any managed .NET assemblies, because the .NET runtime is not loaded. + - No dynamic loading, reflection, or runtime code-generation is possible. + - Some .NET APIs and libraries are not compatible with AOT. +For more details, see https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/ + +### Enabling AOT in a C# project +To set up a project for building a Node.js addon module using C# Native AOT, start with the +[steps for building a regular .NET module](./js-dotnet-module). + +Then enable .NET Native AOT compilation in the C# project: + 1. Make sure the `TargetFramework` is .NET 8 or later. + 2. Add the publishing properties to the project file: +```xml +true +true +bin +true +pkg +``` + 3. Publish the project to produce the native module (with `.node` extension) and npm package + (with `.tgz` extension): +```shell +dotnet publish +``` + +A native module does not depend on the `node-api-dotnet` package, so it can be removed from the +JavaScript project's `package.json`. Then update the JavaScript code to `require()` the .NET +AOT module directly. Be sure to reference the published `.node` file location, which might be +different from the built `.dll` location. + +::: code-group +```JavaScript [CommonJS] +const ExampleModule = require('./bin/ExampleModule'); +``` +```JavaScript [ES] +import ExampleModule from './bin/ExampleModule'; +``` \ No newline at end of file diff --git a/docs/scenarios/js-dotnet-dynamic.md b/docs/scenarios/js-dotnet-dynamic.md new file mode 100644 index 00000000..8fcc84db --- /dev/null +++ b/docs/scenarios/js-dotnet-dynamic.md @@ -0,0 +1,124 @@ +# Dynamically invoke .NET APIs from JavaScript + +For examples of this scenario, see +[/examples/dynamic-invoke/](https://github.com/microsoft/node-api-dotnet/blob/main/examples/dynamic-invoke/) or +[/examples/semantic-kernel/](https://github.com/microsoft/node-api-dotnet/blob/main/examples/semantic-kernel/). + +1. Create a `.csproj` project (without any `.cs` source files) that will manage restoring nuget + packages for .NET assemblies used by JS: + ```xml + + + net6.0 + bin + commonjs// [!code highlight] + + + // [!code highlight] + + + + + ``` + - The `TargetFramework` should match the version of .NET that the JS application will load. + - Add `` elements for any .NET packages you intend to use from JavaScript. + - Add a reference to the `Microsoft.JavaScript.NodeApi.Generator` package to get automatic generation + of TS type-definitions for the referenced system assemblies and nuget packages. + - Change `NodeApiAssemblyJSModuleType` to `esm` if using ES modules. + - For convenience the `OutDir` can be simply set to `bin` because there are no object files + or debug/release builds involved. The referenced assemblies (and their dependencies) + will be placed there. + + Build the project to restore the packages, place assemblies in the `bin` directory, and generate + type definitions: + ```shell + dotnet build + ``` + +2. Initialize a JavaScript project if you don't have one already. (It can be in the same directory + as the C# project, or a separate directory.) Then add a dependency on the + `node-api-dotnet` npm package: + ```shell + npm init + npm install node-api-dotnet + ``` + +3. Import the `node-api-dotnet` package in your JavaScript or TypeScript code. The import syntax + depends on the [module system](https://nodejs.org/api/esm.html) the current project is using: + CommonJS or ES. + ::: code-group + ```JavaScript [ES (TS or JS)] + import dotnet from 'node-api-dotnet'; + ``` + ```TypeScript [CommonJS (TS)] + import * as dotnet from 'node-api-dotnet'; + ``` + ```JavaScript [CommonJS (JS)] + const dotnet = require('node-api-dotnet'); + ``` + ::: + + To load a specific version of .NET, append the target framework moniker to the module name. + A `.js` suffix is required when using ES modules, optional with CommonJS. + ::: code-group + ```JavaScript [ES (TS or JS)] + import dotnet from 'node-api-dotnet/net6.0.js'; + ``` + ```TypeScript [CommonJS (TS)] + import * as dotnet from 'node-api-dotnet/net6.0'; + ``` + ```JavaScript [CommonJS (JS)] + const dotnet = require('node-api-dotnet/net6.0'); + ``` + ::: + Currently the supported target frameworks are `net472`, `net6.0`, and `net8.0`. + +4. Load one or more .NET packages using the generated `.js` files: + ::: code-group + ```JavaScript [ES (TS or JS)] + import './bin/Example.Package.js'; + import './bin/Example.Package.Two.js'; + ``` + ```JavaScript [CommonJS (TS or JS)] + require('./bin/Example.Package.js'); + require('./bin/Example.Package.Two.js'); + ``` + ::: + :warning: Do not assign the results of these `require`/`import` statements. The assemblies are + all loaded into the `dotnet` object (explained in the next step). + + If any of the loaded assemblies depends on other assemblies outside the core framework, they + will be automatically loaded from the same directory. Building the `.csproj` should take care + of bin-placing all dependencies together. If some dependencies are are in another location, + set up a `resolving` event handler _before_ loading the target assembly: + ```JavaScript + dotnet.addListener('resolving', (name, version) => { + const filePath = path.join(__dirname, 'bin', name + '.dll'); + if (fs.existsSync(filePath)) dotnet.load(filePath); + }); + ``` + +5. Namespaces and types from the loaded assemblies are projected onto the top-level `dotnet` object. + When loading multiple .NET assemblies, types from all assemblies are merged into the same + namespace hierarchy. + + It is convenient (and more efficient!) to create aliases for + namespace-qualified .NET types, to avoid repeating the namespace every time. + ```JavaScript + const ExampleStaticClass = dotnet.ExampleNamespace.ExampleStaticClass; + const ExampleClass = dotnet.ExampleNamespace.ExampleClass; + ExampleStaticClass.ExampleMethod(); + const exampleObj = new ExampleClass(...args); + ``` + Of course you can access properites, pass arguments to methods, get return values, and so on. + Most types get automatically marshalled between JavaScript and .NET as you'd expect. For + details, see the [JavaScript / .NET type mappings reference](../reference/js-dotnet-types). + + You should notice your IDE offers documentation-comments and member completion from the type + definitions, and if writing TypeScript code the TypeScript compiler will check against the + type definitions. + + .NET system assemblies can also be loaded and used. For example, import `System.Runtime.js` to + get the core types, `System.Console.js` to get console APIs, etc. Type definitions for those + two assembiles are generated by default; to generate typedefs for additional system assemblies, + add items to the `NodeApiSystemReferenceAssembly` MSBuild item-list in the project. diff --git a/docs/scenarios/js-dotnet-module.md b/docs/scenarios/js-dotnet-module.md new file mode 100644 index 00000000..75e38630 --- /dev/null +++ b/docs/scenarios/js-dotnet-module.md @@ -0,0 +1,103 @@ +# Develop a Node.js addon module in C# + +For a minimal example of this scenario, see +[/examples/dotnet-module/](https://github.com/microsoft/node-api-dotnet/blob/main/examples/dotnet-module/). + +1. Create a .NET Class library project that targets .NET 6 or later. (.NET 8 for AOT.) + ```shell + mkdir ExampleModule + cd ExampleModule + dotnet new classlib --framework net6.0 + ``` + +2. Add a reference to the `Microsoft.JavaScript.NodeApi` and + `Microsoft.JavaScript.NodeApi.Generator` packages: + ```shell + dotnet add package --prerelease Microsoft.JavaScript.NodeApi + dotnet add package --prerelease Microsoft.JavaScript.NodeApi.Generator + ``` + + Afterward you should have the two references in your project file: + ```xml + + + + + ``` + +3. Add one or more public types to the project with the `[JSExport]` attribute. Types tagged + with this attribute, along with any types referenced in public properties or methods of tagged + types, are exported to JavaScript. It is also possible to export _module-level_ + properties and methods by using `[JSExport]` on `public static` properties or methods of a class + that is otherwise not exported. + ```C# + using Microsoft.JavaScript.NodeApi; + + [JSExport] + public class Example + { + ``` + +4. Build the project, to produce the assembly (`.dll`) file. + ```shell + dotnet build + ``` + The build also automaticaly produces a `.d.ts` file with type definitions for APIs in the + module that are exported to JavaScript. + + ::: tip :sparkles: TIP + If you're curious, you can check out the generated marshalling code for exported APIs at
+ `obj/{Configuration}/{TargetFramerwork}/{RuntimeIdentifier}/generated/ + Microsoft.JavaScript.NodeApi.Generator/Microsoft.JavaScript.NodeApi.Generator.ModuleGenerator/`. + Read about + [marshalling code-generation](../features/js-dotnet-marshalling#marshalling-code-generation) + to learn more about what's going on in there. + ::: + +5. Switching over to the JavaScript project, add a dependency on the `node-api-dotnet` npm package: + ```shell + npm install node-api-dotnet + ``` + +6. Import the `node-api-dotnet` package in your JavaScript or TypeScript code. The import syntax + depends on the [module system](https://nodejs.org/api/esm.html) the current project is using. + ::: code-group + ```JavaScript [ES (TS or JS)] + import dotnet from 'node-api-dotnet'; + ``` + ```TypeScript [CommonJS (TS)] + import * as dotnet from 'node-api-dotnet'; + ``` + ```JavaScript [CommonJS (JS)] + const dotnet = require('node-api-dotnet'); + ``` + ::: + + To load a specific version of .NET, append the target framework moniker to the module name. + A `.js` suffix is required when using ES modules, optional with CommonJS. + ::: code-group + ```JavaScript [ES (TS or JS)] + import dotnet from 'node-api-dotnet/net8.0.js'; + ``` + ```TypeScript [CommonJS (TS)] + import * as dotnet from 'node-api-dotnet/net8.0'; + ``` + ```JavaScript [CommonJS (JS)] + const dotnet = require('node-api-dotnet/net8.0'); + ``` + ::: + Currently the supported target frameworks are `net472`, `net6.0`, and `net8.0`. + +7. Load your .NET module assembly from its path using the `dotnet.require()` function: + ```JavaScript + const ExampleModule = dotnet.require('./bin/ExampleModule'); + ``` +8. Exported APIs in the assembly are projected as properties on the loaded module object. So then + you can use those to call static methods, construct instances of classes, etc: + ```JavaScript + ExampleModule.StaticClass.ExampleMethod(); + const exampleObj = new ExampleModule.ExampleClass(...args); + ``` + Of course you can access properites, pass arguments to methods, get return values, and so on. + Most types get automatically marshalled between JavaScript and .NET as you'd expect. For + details, see the [JavaScript / .NET type mappings reference](../reference/js-dotnet-types). diff --git a/docs/support.md b/docs/support.md new file mode 100644 index 00000000..bafd1fa6 --- /dev/null +++ b/docs/support.md @@ -0,0 +1,8 @@ +--- +prev: false +next: false +--- + +# Support + +[node-api-dotnet/issues](https://github.com/microsoft/node-api-dotnet/issues) diff --git a/docs/tools/XmlDocMarkdown.cs b/docs/tools/XmlDocMarkdown.cs new file mode 100644 index 00000000..edace9af --- /dev/null +++ b/docs/tools/XmlDocMarkdown.cs @@ -0,0 +1,2 @@ +using XmlDocMarkdown.Core; +return XmlDocMarkdownApp.Run(args); diff --git a/docs/tools/XmlDocMarkdown.csproj b/docs/tools/XmlDocMarkdown.csproj new file mode 100644 index 00000000..da711fd9 --- /dev/null +++ b/docs/tools/XmlDocMarkdown.csproj @@ -0,0 +1,8 @@ + + + Exe + + + + + diff --git a/docs/tools/build-dotnet-api-docs.js b/docs/tools/build-dotnet-api-docs.js new file mode 100644 index 00000000..f4e2b165 --- /dev/null +++ b/docs/tools/build-dotnet-api-docs.js @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const childProcess = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +const assemblyNamePrefix = 'Microsoft.JavaScript.'; +const docsTargetFramework = 'net6.0'; +const docsConfiguration = 'Release'; +const ridPlatform = + process.platform === 'win32' ? 'win' : + process.platform === 'darwin' ? 'osx' : + process.platform; +const ridArch = process.arch === 'ia32' ? 'x86' : process.arch; +const rid = `${ridPlatform}-${ridArch}`; + +const rootDir = path.resolve(__dirname, '../..'); +const srcDir = path.join(rootDir, 'src'); +const binDir = path.join(rootDir, 'out', 'bin'); +const outDir = path.resolve(__dirname, '../reference/dotnet'); + +const projectList = [ + { name: 'NodeApi' }, + { name: 'NodeApi.DotNetHost', references: ['NodeApi'] }, +]; + +try { + console.log('Creating directory: ' + outDir); + if (fs.existsSync(outDir)) fs.rmSync(outDir, { recursive: true, force: true }); + fs.mkdirSync(outDir, { recursive: true }); + + // Build markdown for each project, and merge all indexes into a combined index file. + let indexMarkdown = ''; + for (let project of projectList) { + indexMarkdown += buildProjectMarkdownDocs(project.name, project.references); + } + + console.log('Generating navigation tree: ' + path.join(outDir, 'nav.mts')); + const nav = generateNavigationTree('package', outDir); + fs.writeFileSync(path.join(outDir, 'nav.mts'), 'export default ' + JSON.stringify(nav, null, 2)); + + // Remove the assembly headers in the combined index file and insert a new top-level header. + indexMarkdown = '# .NET API Reference\n' + indexMarkdown.replace(/^# .*$/gm, ''); + + fs.writeFileSync(path.join(outDir, 'index.md'), indexMarkdown); + + console.log('Post-processing markdown files.'); + processMarkdownFiles(outDir); +} catch (e) { + console.error(e.message || e); + process.exit(1); +} + +/** Log a command and execute it synchronously. */ +function exec(cmd) { + console.log(cmd); + try { + childProcess.execSync(cmd); + } catch (e) { + if (e.status) throw new Error(`Command executed with status: ${e.status}`); + throw e; + } +} + +/** Run `dotnet build` with options to produce XML doc files for a project. */ +function buildProjectXmldocs(projectName, assemblyName) { + const projectDir = path.join(srcDir, projectName); + console.log('Building project: ' + projectDir); + exec(`dotnet build "${projectDir}" -c ${docsConfiguration} ` + + `-f ${docsTargetFramework} -p:GenerateDocumentationFile=true -p:NoWarn=CS1591 -t:Rebuild`); + const projectBinDir = path.join( + binDir, + docsConfiguration, + projectName, + docsTargetFramework, + rid); + const assemblyFilePath = path.join(projectBinDir, assemblyName + '.dll'); + if (!fs.existsSync(assemblyFilePath)) { + throw new Error('Assembly file was not found at ' + assemblyFilePath); + } + const xmldocFilePath = path.join(projectBinDir, assemblyName + '.xml'); + if (!fs.existsSync(xmldocFilePath)) { + throw new Error('XML doc file was not found at ' + xmldocFilePath); + } + return assemblyFilePath; +} + +/** Use the XmlDocMarkdown tool to generate markdown from the XML doc files. */ +function buildProjectMarkdownDocs(projectName, projectReferences) { + const assemblyName = assemblyNamePrefix + projectName; + const assemblyFilePath = buildProjectXmldocs(projectName, assemblyName); + const assemblyReferences = (projectReferences ?? []).map((p) => assemblyNamePrefix + p); + + console.log('Generating markdown: ' + assemblyName); + + // A .csproj file for the XmlDocMarkdown tool is in the same directory as this script. + // Recommended usage is to build the tool from source: https://ejball.com/XmlDocMarkdown/#usage + const xmlDocMdRunCommand = + `dotnet run -c Release -f ${docsTargetFramework} --project ${__dirname} --`; + + // Ref structs are obsolete: https://turnerj.com/blog/ref-structs-are-technically-obsolete + // So the --obsolete option is required to include them in docs. + exec(`${xmlDocMdRunCommand} "${assemblyFilePath}" "${outDir}" --obsolete ` + + `--source "https://github.com/microsoft/node-api-dotnet/tree/main/src/${projectName}"` + + assemblyReferences.map((r) => ` --external "${r}"`).join('')); + + const assemblyMarkdownFile = path.join(outDir, assemblyName + '.md'); + const assemblyMarkdown = fs.readFileSync(assemblyMarkdownFile, 'utf8'); + + return assemblyMarkdown; +} + +/** Process all generated markdown files and apply some minor edits. */ +function processMarkdownFiles(directory) { + fs.readdirSync(directory, { recursive: true}).forEach((file) => { + if (/\.md$/.test(file)) { + const filePath = path.join(directory, file); + let markdown = fs.readFileSync(filePath, 'utf8'); + markdown = processMarkdown(markdown); + fs.writeFileSync(filePath, markdown); + } + }); +} + +/** Apply some transformations on a markdown string. */ +function processMarkdown(md) { + // Escape some special chars in markdown tables because they cause problems with vitepress: + // https://github.com/vuejs/vitepress/issues/449 + md = md + .replace(/(?<=\|[^|]*)(? /\.md$/.test(f)) + .map((f) => f.substr(0, f.length - 3)) + .sort() + .forEach((file) => { + extractNavItemsFromMarkdown(rootDir, subDir, path.join(currentDir, file + '.md'), items); + }); + + // Look for nested types which are in the same directory as their containing type. + if (subDir) { + fs.readdirSync(path.dirname(currentDir)) + .filter((f) => /\.md$/.test(f)) + .map((f) => f.substr(0, f.length - 3)) + .filter((f) => f.startsWith(path.basename(currentDir) + '.')) + .sort() + .forEach((file) => { + extractNavItemsFromMarkdown( + rootDir, + path.dirname(subDir), + path.join(path.dirname(currentDir), file + '.md'), + items, + true); + }); + } + + return categorizeNavItems(itemType, items); +} + +function extractNavItemsFromMarkdown(rootDir, subDir, markdownFile, items, allowNested) { + const referencePath = '/reference/dotnet/'; + let file = path.basename(markdownFile, '.md'); + + // The top-level markdown files are per-assembly, but the nav should start with namespaces. + let headers = getMarkdownFileHeaders( + markdownFile, + subDir ? '#' : '##'); + + if (/ \(\d.*\)$/.test(headers[0])) { + // Merge overloads into a single item. + headers = [headers[0].replace(/ \(\d.*\)$/, '')]; + } + + headers.forEach((header) => { + let link = path.join(referencePath, subDir ?? '', file).replace(/\\/g, '/'); + + if (/ namespace$/.test(header)) { + // Fix namespace links to point to subheaders in the merged index. + link = referencePath + '#' + + header.replace(/\.| /g, '-').toLowerCase(); + + // Use the namespace name as the file (directory) name when recursing. + file = header.replace(/ namespace$/, ''); + } else if (file.includes('.')) { + if (!allowNested) return; + header = header.replace(' ' + file.substr(0, file.indexOf('.')) + '.', ''); + } + + // If there's a matching subdirectory, recurse to find sub-items. + let subItems = undefined; + if (fs.existsSync(path.join(path.dirname(markdownFile), file)) && + fs.lstatSync(path.join(path.dirname(markdownFile), file)).isDirectory() + ) { + const itemType = header.substr(header.indexOf(' ') + 1); + subItems = generateNavigationTree(itemType, rootDir, path.join(subDir ?? '', file)); + } + + // Shorten some header suffixes so they fit better in the sidebar. + header = header + .replace(/ namespace$/, '') + .replace('structure', 'struct') + .replace('enumeration', 'enum'); + + items.push({ + text: header, + link: link, + items: subItems, + collapsed: subItems ? true : undefined, + }); + }); +} + +function categorizeNavItems(containerType, items) { + const typeCategories = [ + 'Interfaces', + 'Classes', + 'Structs', + 'Enums', + 'Delegates', + ]; + const memberCategories = [ + 'Constructors', + 'Properties', + 'Methods', + 'Operators', + 'Events', + 'Fields', + 'Types', + ]; + let categories = []; + switch (containerType) { + case 'namespace': + categories = typeCategories; + break; + case 'interface': + case 'class': + case 'structure': + categories = memberCategories; + break; + default: + return items; + } + + function getCategorySuffix(category) { + return ' ' + ( + category === 'Properties' ? 'property' : + category === 'Classes' ? 'class' : + category.toLowerCase().replace(/s$/, '')); + } + + const categorizedItems = []; + + for (let category of categories) { + const suffixes = category == 'Types' ? + typeCategories.map(getCategorySuffix) : [getCategorySuffix(category)]; + const categoryItems = items.filter((i) => suffixes.some((s) => i.text.endsWith(s))); + if (categoryItems.length > 0) { + categorizedItems.push({ + text: category, + items: categoryItems, + collapsed: true, + }); + } + } + + return categorizedItems; +} + +/** Extract headers from a markdown file. */ +function getMarkdownFileHeaders(file, headerPrefix) { + const markdown = fs.readFileSync(file, 'utf8'); + const headerRegex = new RegExp(`^${headerPrefix} (.*)$`, 'gm'); + const headerMatches = markdown.matchAll(headerRegex); + const headers = [...headerMatches].map((m) => m[1]); + return headers; +} diff --git a/docs/tools/build-js-api-docs.js b/docs/tools/build-js-api-docs.js new file mode 100644 index 00000000..56ffb99a --- /dev/null +++ b/docs/tools/build-js-api-docs.js @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const fs = require('node:fs'); +const path = require('node:path'); +const typedoc = require('typedoc'); + +const srcDir = path.resolve(__dirname, '../../src/node-api-dotnet'); +const outDir = path.resolve(__dirname, '../reference/js'); + +const typedefsFile = path.join(srcDir, 'index.d.ts'); +const jsonFile = path.join(outDir, 'api.json'); +const markdownFile = path.join(outDir, 'index.md'); + +console.log('Creating directory: ' + outDir); +if (fs.existsSync(outDir)) fs.rmSync(outDir, { recursive: true, force: true }); +fs.mkdirSync(outDir, { recursive: true }); + +exportJsdocToJson(typedefsFile, jsonFile) + .then(() => convertJsonToMarkdown(jsonFile, markdownFile)) + .catch ((e) => { + console.error(e.message || e); + process.exit(1); + }); + +async function exportJsdocToJson(typedefsFile, jsonFile) { + if (!fs.existsSync(typedefsFile)) { + throw new Error(`File not found: ${typedefsFile}`); + } + + console.log('Exporting JSDoc from ' + typedefsFile); + + const app = await typedoc.Application.bootstrap({ + entryPoints: [typedefsFile], + tsconfig: path.join(path.dirname(typedefsFile), 'tsconfig.json'), + exclude: '**/node_modules/**', + excludeExternals: true, + excludePrivate: true, + excludeProtected: true, + excludeInternal: true, + readme: 'none', + }); + app.options.addReader(new typedoc.TSConfigReader()); + const project = await app.convert(); + if (!project) { + throw new Error('Failed to convert TypeScript to documentation.'); + } + + await app.generateJson(project, jsonFile); +} + +function convertJsonToMarkdown(jsonFile, markdownFile) { + console.log('Generating markdown from ' + jsonFile); + + /** @type {typedoc.Models.ProjectReflection} */ + const project = JSON.parse(fs.readFileSync(jsonFile, 'utf8')); + + let markdown = `--- +editLink: false +outline: deep +--- +`; + + markdown += `# ${project.name} package\n`; + + if (project.comment?.summary) { + markdown += '\n' + commentToMarkdown(project.comment.summary) + '\n'; + } + + const propertyReflections = project.children + .filter((item) => item.kind === typedoc.Models.ReflectionKind.Variable); + if (propertyReflections.length > 0) { + markdown += '\n## Properties\n'; + for (const propertyReflection of propertyReflections) { + markdown += convertPropertyReflectionToMarkdown(propertyReflection); + } + } + + const functionReflections = project.children + .filter((item) => item.kind === typedoc.Models.ReflectionKind.Function); + if (functionReflections.length > 0) { + markdown += '\n## Methods\n'; + for (const functionReflection of functionReflections) { + markdown += convertFunctionReflectionToMarkdown(functionReflection); + } + } + + fs.writeFileSync(markdownFile, markdown); + console.log('Generated ' + markdownFile); +} + +function convertPropertyReflectionToMarkdown( + /** @type {typedoc.Models.ProjectReflection} */ + item, +) { + let markdown = `\n### ${item.name} property\n`; + markdown += '```TypeScript\n'; + markdown += `const dotnet.${item.name}: ${typeToMarkdown(item.type)}\n`; + markdown += '```\n'; + markdown += commentToMarkdown(item.comment?.summary) + '\n'; + return markdown; +} + +function convertFunctionReflectionToMarkdown( + /** @type {typedoc.Models.ProjectReflection} */ + item, +) { + let markdown = `\n### ${item.name} method\n`; + for (let signature of item.signatures) { + let parameters = signature.parameters.map( + (param) => `${param.name}: ${typeToMarkdown(param.type)}`); + if (parameters.length > 1) { + parameters = parameters.map((p) => '\n ' + p + ','); + parameters[parameters.length - 1] += '\n'; + } + markdown += '```TypeScript\n'; + markdown += `dotnet.${item.name}(${parameters.join('')}): ${signature.type?.name}\n`; + markdown += '```\n'; + markdown += commentToMarkdown(signature.comment?.summary) + '\n'; + + for (let param of signature.parameters.filter((p) => p.comment?.summary)) { + markdown += `- **${param.name}**: ${commentToMarkdown(param.comment?.summary)}\n`; + } + if (signature.comment?.blockTags?.length > 0) { + const returnsTag = signature.comment.blockTags.find((t) => t.tag === '@returns'); + if (returnsTag) { + markdown += `- Returns: ${commentToMarkdown(returnsTag.content)}\n`; + } + + const descriptionTag = signature.comment.blockTags.find((t) => t.tag === '@description'); + if (descriptionTag) { + markdown += '\n' + commentToMarkdown(descriptionTag.content) + '\n'; + } + } + } + return markdown; +} + +function typeToMarkdown( + /** @type {typedoc.Models.SomeType} */ + type, +) { + if (type?.type === 'literal') { + return JSON.stringify(type.value); + } else if (type?.name) { + return type.name; + } else if (type.declaration?.signatures?.length === 1) { + const signature = type.declaration.signatures[0]; + const parameters = signature.parameters.map( + (param) => `${param.name}: ${typeToMarkdown(param.type)}`); + return `(${parameters.join(', ')}) => ${typeToMarkdown(signature.type)}`; + } else { + return 'unknown'; + } +} + +function commentToMarkdown( + /** @type {typedoc.Models.CommentDisplayPart[]} */ + comment, +) { + return (comment || []).map((part) => part.text).join(''); +} diff --git a/Docs/typescript.md b/docs/typescript.md similarity index 100% rename from Docs/typescript.md rename to docs/typescript.md diff --git a/src/NodeApi/DotNetHost/MSCorEE.cs b/src/NodeApi/DotNetHost/MSCorEE.cs index ec3349a5..690b066e 100644 --- a/src/NodeApi/DotNetHost/MSCorEE.cs +++ b/src/NodeApi/DotNetHost/MSCorEE.cs @@ -11,7 +11,7 @@ namespace Microsoft.JavaScript.NodeApi.DotNetHost; /// /// P/Invoke declarations and supporting code for the .net Framework 4.x CLR hosting APIs defined -/// in MetaHost.h & mscoree.h (mscoree.dll). +/// in MetaHost.h and mscoree.h (mscoree.dll). /// /// /// https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/clr-hosting-interfaces-added-in-the-net-framework-4-and-4-5 diff --git a/src/NodeApi/DotNetHost/NativeHost.cs b/src/NodeApi/DotNetHost/NativeHost.cs index fed942f3..b5a52ecc 100644 --- a/src/NodeApi/DotNetHost/NativeHost.cs +++ b/src/NodeApi/DotNetHost/NativeHost.cs @@ -16,7 +16,7 @@ namespace Microsoft.JavaScript.NodeApi.DotNetHost; /// /// When AOT-compiled, exposes a native entry-point that supports loading the .NET runtime -/// and the Node API . +/// and the Node API managed host. /// internal unsafe partial class NativeHost : IDisposable { diff --git a/src/NodeApi/Interop/EmptyAttributes.cs b/src/NodeApi/Interop/EmptyAttributes.cs index 52b57196..74595488 100644 --- a/src/NodeApi/Interop/EmptyAttributes.cs +++ b/src/NodeApi/Interop/EmptyAttributes.cs @@ -37,16 +37,6 @@ public sealed class DoesNotReturnIfAttribute : Attribute } } -namespace System -{ - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public sealed class UnmanagedCallersOnlyAttribute : Attribute - { - public UnmanagedCallersOnlyAttribute() { } - public Type[]? CallConvs; - } -} - namespace System.Runtime.CompilerServices { public sealed class IsExternalInit diff --git a/src/NodeApi/Interop/JSCallbackOverload.cs b/src/NodeApi/Interop/JSCallbackOverload.cs index 6ff432d3..d64a1602 100644 --- a/src/NodeApi/Interop/JSCallbackOverload.cs +++ b/src/NodeApi/Interop/JSCallbackOverload.cs @@ -67,9 +67,9 @@ public static JSCallbackDescriptor CreateDescriptor( /// not loaded until the first call. /// /// The method name. - /// Function that returns an array of objects each having parameter - /// information for one overload. The function is called only once, on the first invocation - /// of the callback. + /// Function that returns an array of objects each having + /// parameter information for one overload. The function is called only once, on the first + /// invocation of the callback. /// Callback descriptor that can be used for marshalling the method call. public static JSCallbackDescriptor CreateDescriptor( string? name, diff --git a/src/NodeApi/Interop/JSClassBuilderOfT.cs b/src/NodeApi/Interop/JSClassBuilderOfT.cs index 8849803b..e13254dd 100644 --- a/src/NodeApi/Interop/JSClassBuilderOfT.cs +++ b/src/NodeApi/Interop/JSClassBuilderOfT.cs @@ -162,7 +162,7 @@ public JSValue DefineStaticClass() /// /// A JS class defined this way may not be constructed directly from JS. An instance of the /// class may be constructed when passing a .NET object (that implements the interface) to JS - /// via . + /// via . /// public JSValue DefineInterface() { diff --git a/src/NodeApi/Interop/JSModuleAttribute.cs b/src/NodeApi/Interop/JSModuleAttribute.cs index 48ace315..5e173bab 100644 --- a/src/NodeApi/Interop/JSModuleAttribute.cs +++ b/src/NodeApi/Interop/JSModuleAttribute.cs @@ -17,7 +17,7 @@ namespace Microsoft.JavaScript.NodeApi.Interop; /// interface. An instance of the class will be constructed when the module is loaded, and disposed /// when the module is unloaded if it implements . Public non-static /// properties and methods on the same module class are automatically exported. Those exports are -/// merged with any additional items in the assembly (other classes, static properties & methods, +/// merged with any additional items in the assembly (other classes, static properties and methods, /// etc) that are tagged with . /// /// If is applied to a public static method, then that module diff --git a/src/NodeApi/Interop/JSRuntimeContext.cs b/src/NodeApi/Interop/JSRuntimeContext.cs index 5e42dc88..9492608c 100644 --- a/src/NodeApi/Interop/JSRuntimeContext.cs +++ b/src/NodeApi/Interop/JSRuntimeContext.cs @@ -242,7 +242,7 @@ public JSFunction ImportFunction /// Registers a class JS constructor, enabling automatic JS wrapping of instances of the class. /// /// JS class constructor function returned from - /// + /// /// The JS constructor. internal JSValue RegisterClass(JSValue constructorFunction) where T : class { @@ -296,7 +296,7 @@ private JSValue GetClassConstructor() where T : class /// Attaches an object to a JS wrapper, and saves a weak reference to the wrapper. /// /// JS object passed as the 'this' argument to the constructor callback - /// for . + /// for . /// New or existing instance of the class to be wrapped, /// passed as a JS "external" value. /// The JS wrapper. @@ -564,7 +564,7 @@ public JSValue GetOrCreateCollectionWrapper( /// Registers a struct JS constructor, enabling instantiation of JS wrappers for the struct. /// /// JS struct constructor function returned from - /// + /// /// The JS constructor. internal JSValue RegisterStruct(JSValue constructorFunction) where T : struct { @@ -610,8 +610,8 @@ public JSValue CreateStruct() where T : struct /// (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. + /// Both and + /// are null. /// The or /// property was not initialized. public JSValue Import( @@ -673,8 +673,8 @@ public JSValue Import( /// (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. + /// Both and + /// are null. /// The or /// property was not initialized. public async Task ImportAsync( diff --git a/src/NodeApi/Interop/JSSynchronizationContext.cs b/src/NodeApi/Interop/JSSynchronizationContext.cs index 8ef8f968..5e6fd181 100644 --- a/src/NodeApi/Interop/JSSynchronizationContext.cs +++ b/src/NodeApi/Interop/JSSynchronizationContext.cs @@ -23,7 +23,8 @@ namespace Microsoft.JavaScript.NodeApi.Interop; /// /// Code that makes explicit use of .NET threads or thread pools may need to capture the /// context (before switching off the JS thread) -/// and hold it for later use to call back to JS via , +/// and hold it for later use to call back to JS via +/// , /// , or . /// public abstract class JSSynchronizationContext : SynchronizationContext, IDisposable @@ -89,7 +90,7 @@ public void Post(Action action, bool allowSync = false) /// /// Runs an asynchronous action on the JS thread, without waiting for completion. /// - /// The action to run. + /// The action to run. /// True to allow the action to run immediately if the current /// synchronization context is this one. By default the action will always be scheduled /// for later execution. diff --git a/src/NodeApi/Interop/JSThreadSafeFunction.cs b/src/NodeApi/Interop/JSThreadSafeFunction.cs index d9910697..a7dbef68 100644 --- a/src/NodeApi/Interop/JSThreadSafeFunction.cs +++ b/src/NodeApi/Interop/JSThreadSafeFunction.cs @@ -205,7 +205,9 @@ private static readonly unsafe delegate* unmanaged[Cdecl] s_defaultCallJS = &DefaultCallJS; #endif +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe void FinalizeFunctionData(napi_env env, nint _, nint hint) { GCHandle functionDataHandle = GCHandle.FromIntPtr(hint); @@ -217,7 +219,9 @@ private static unsafe void FinalizeFunctionData(napi_env env, nint _, nint hint) functionDataHandle.Free(); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe void CustomCallJS(napi_env env, napi_value jsCallback, nint context, nint data) { if (env.IsNull && jsCallback.IsNull) @@ -249,7 +253,9 @@ private static unsafe void CustomCallJS(napi_env env, napi_value jsCallback, nin } } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe void DefaultCallJS(napi_env env, napi_value jsCallback, nint context, nint data) { if (env.IsNull && jsCallback.IsNull) diff --git a/src/NodeApi/Interop/NodeStream.Proxy.cs b/src/NodeApi/Interop/NodeStream.Proxy.cs index c994a316..60346710 100644 --- a/src/NodeApi/Interop/NodeStream.Proxy.cs +++ b/src/NodeApi/Interop/NodeStream.Proxy.cs @@ -20,7 +20,7 @@ public partial class NodeStream /// /// Name of the stream class. /// Callback that constructs an instance of the .NET - /// subclass. + /// subclass. /// Additional static properties on the stream subclass. /// The class object. internal static JSValue DefineStreamClass( diff --git a/src/NodeApi/JSCallback.cs b/src/NodeApi/JSCallback.cs index abe4cce7..cea08635 100644 --- a/src/NodeApi/JSCallback.cs +++ b/src/NodeApi/JSCallback.cs @@ -3,6 +3,17 @@ namespace Microsoft.JavaScript.NodeApi; +/// +/// Represents a low-level function or method call or callback from JavaScript into .NET. +/// +/// Provides access to the arguments for the call, along with the `this` +/// argument and an optional context object. +/// The return value as a JS value. public delegate JSValue JSCallback(JSCallbackArgs args); +/// +/// Represents a low-level void function or method call or callback from JavaScript into .NET. +/// +/// Provides access to the arguments for the call, along with the `this` +/// argument and an optional context object. public delegate void JSActionCallback(JSCallbackArgs args); diff --git a/src/NodeApi/JSCallbackArgs.cs b/src/NodeApi/JSCallbackArgs.cs index 8ef330d5..82d04cc3 100644 --- a/src/NodeApi/JSCallbackArgs.cs +++ b/src/NodeApi/JSCallbackArgs.cs @@ -7,6 +7,13 @@ namespace Microsoft.JavaScript.NodeApi; +/// +/// Provides access to the arguments for a low-level JavaScript function call or callback, along +/// with the `this` argument and an optional context object. +/// +/// +/// This type is a `ref struct` meaning it can only be used on the call stack. +/// public readonly ref struct JSCallbackArgs { private readonly napi_value _thisArg; @@ -41,12 +48,28 @@ internal JSCallbackArgs( internal JSValueScope Scope { get; } + /// + /// Gets the `this` argument for the call. + /// public JSValue ThisArg => new(_thisArg, Scope); + /// + /// Gets the argument at the specified (zero-based) index. + /// + /// + /// If the index is out of range, this property returns `default(JSValue)` which is equivalent + /// to JS `undefined`. + /// public JSValue this[int index] => index < _args.Length ? new(_args[index], Scope) : default; + /// + /// Gets the number of arguments. + /// public int Length => _args.Length; + /// + /// Gets the optional context object that was provided when the callback was registered. + /// public object? Data { get; } internal static void GetDataAndLength( diff --git a/src/NodeApi/JSDispatcherQueue.cs b/src/NodeApi/JSDispatcherQueue.cs index 3b43cde0..16336a35 100644 --- a/src/NodeApi/JSDispatcherQueue.cs +++ b/src/NodeApi/JSDispatcherQueue.cs @@ -44,7 +44,7 @@ public sealed class JSDispatcherQueue [ThreadStatic] private static JSDispatcherQueue? s_currentQueue; - public event EventHandler? ShutdownStarting; + public event EventHandler? ShutdownStarting; public event EventHandler? ShutdownCompleted; @@ -183,7 +183,7 @@ internal void Shutdown(TaskCompletionSource completion) _onShutdownCompleted = completion; ShutdownStarting?.Invoke( - this, new JSDispatcherQueueShutdownStartingEventArgs(CreateDeferral)); + this, new ShutdownStartingEventArgs(CreateDeferral)); DecrementDeferralCount(); // Decrement the initial _deferralCount == 1. }); @@ -254,6 +254,16 @@ private void DecrementDeferralCount() } } + public sealed class ShutdownStartingEventArgs : EventArgs + { + private readonly Func _getDeferral; + + internal ShutdownStartingEventArgs(Func getDeferral) + => _getDeferral = getDeferral; + + public IDisposable GetDeferral() => _getDeferral(); + } + private readonly struct CurrentQueueHolder : IDisposable { private readonly JSDispatcherQueue? _previousCurrentQueue; @@ -428,13 +438,3 @@ public void Invoke() } } } - -public sealed class JSDispatcherQueueShutdownStartingEventArgs : EventArgs -{ - private readonly Func _getDeferral; - - internal JSDispatcherQueueShutdownStartingEventArgs(Func getDeferral) - => _getDeferral = getDeferral; - - public IDisposable GetDeferral() => _getDeferral(); -} diff --git a/src/NodeApi/JSReference.cs b/src/NodeApi/JSReference.cs index d986f1d6..c5667fb3 100644 --- a/src/NodeApi/JSReference.cs +++ b/src/NodeApi/JSReference.cs @@ -156,6 +156,12 @@ public void MakeStrong() /// on the value. /// /// The reference is disposed. + /// The reference is weak and the value is not + /// available. + /// + /// This method may be called from any thread. The delegate is + /// invoked on the JS thread. + /// public void Run(Action action) { void GetValueAndRunAction() @@ -186,6 +192,12 @@ void GetValueAndRunAction() /// on the value. /// /// The reference is disposed. + /// The reference is weak and the value is not + /// available. + /// + /// This method may be called from any thread. The delegate is + /// invoked on the JS thread. + /// public T Run(Func action) { T GetValueAndRunAction() diff --git a/src/NodeApi/JSValue.cs b/src/NodeApi/JSValue.cs index be600478..07289740 100644 --- a/src/NodeApi/JSValue.cs +++ b/src/NodeApi/JSValue.cs @@ -1181,7 +1181,9 @@ internal static readonly unsafe delegate* unmanaged[Cdecl] s_callFinalizeAction = &CallFinalizeAction; #endif +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif internal static unsafe napi_value InvokeJSCallback( napi_env env, napi_callback_info callbackInfo) { @@ -1189,7 +1191,9 @@ internal static unsafe napi_value InvokeJSCallback( env, callbackInfo, JSValueScopeType.Callback, (descriptor) => descriptor); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value InvokeJSMethod(napi_env env, napi_callback_info callbackInfo) { return InvokeCallback( @@ -1200,7 +1204,9 @@ private static unsafe napi_value InvokeJSMethod(napi_env env, napi_callback_info propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value InvokeJSGetter(napi_env env, napi_callback_info callbackInfo) { return InvokeCallback( @@ -1211,7 +1217,9 @@ private static unsafe napi_value InvokeJSGetter(napi_env env, napi_callback_info propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static napi_value InvokeJSSetter(napi_env env, napi_callback_info callbackInfo) { return InvokeCallback( @@ -1222,7 +1230,9 @@ private static napi_value InvokeJSSetter(napi_env env, napi_callback_info callba propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif internal static unsafe napi_value InvokeJSCallbackNoContext( napi_env env, napi_callback_info callbackInfo) { @@ -1230,7 +1240,9 @@ internal static unsafe napi_value InvokeJSCallbackNoContext( env, callbackInfo, JSValueScopeType.NoContext, (descriptor) => descriptor); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value InvokeJSMethodNoContext(napi_env env, napi_callback_info callbackInfo) { return InvokeCallback( @@ -1241,7 +1253,9 @@ private static unsafe napi_value InvokeJSMethodNoContext(napi_env env, napi_call propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value InvokeJSGetterNoContext(napi_env env, napi_callback_info callbackInfo) { return InvokeCallback( @@ -1252,7 +1266,9 @@ private static unsafe napi_value InvokeJSGetterNoContext(napi_env env, napi_call propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static napi_value InvokeJSSetterNoContext(napi_env env, napi_callback_info callbackInfo) { return InvokeCallback( @@ -1286,7 +1302,9 @@ private static unsafe napi_value InvokeCallback( } } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif internal static unsafe void FinalizeGCHandle(napi_env env, nint data, nint hint) { GCHandle handle = GCHandle.FromIntPtr(data); @@ -1302,7 +1320,9 @@ internal static unsafe void FinalizeGCHandle(napi_env env, nint data, nint hint) } } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif internal static unsafe void FinalizeGCHandleToDisposable(napi_env env, nint data, nint hint) { GCHandle handle = GCHandle.FromIntPtr(data); @@ -1325,7 +1345,9 @@ internal static unsafe void FinalizeGCHandleToDisposable(napi_env env, nint data } } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif internal static unsafe void FinalizeGCHandleToPinnedMemory(napi_env env, nint data, nint hint) { // The GC handle is passed via the hint parameter. @@ -1342,7 +1364,9 @@ internal static unsafe void FinalizeGCHandleToPinnedMemory(napi_env env, nint da } } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe void CallFinalizeAction(napi_env env, nint data, nint hint) { GCHandle gcHandle = GCHandle.FromIntPtr(data); diff --git a/src/NodeApi/JSValueScope.cs b/src/NodeApi/JSValueScope.cs index e908e575..cba1347d 100644 --- a/src/NodeApi/JSValueScope.cs +++ b/src/NodeApi/JSValueScope.cs @@ -123,8 +123,8 @@ public static explicit operator napi_env(JSValueScope scope) /// /// Gets the environment handle for the current thread scope, or throws an exception if /// there is no environment for the current thread. For use only with static operations - /// not related to any ; for value operations use - /// instead. + /// not related to any ; for value operations use the + /// from the value's instead. /// /// No scope was established for the current /// thread. @@ -147,7 +147,7 @@ public static explicit operator napi_env(JSValueScope scope) /// Creates a new instance of a with a specified scope type. /// /// The type of scope to create; default is - /// . + /// . public JSValueScope(JSValueScopeType scopeType = JSValueScopeType.Handle) : this(scopeType, env: default, runtime: default) { diff --git a/src/NodeApi/JSValueScopeClosedException.cs b/src/NodeApi/JSValueScopeClosedException.cs index 370389f9..df446790 100644 --- a/src/NodeApi/JSValueScopeClosedException.cs +++ b/src/NodeApi/JSValueScopeClosedException.cs @@ -13,7 +13,7 @@ namespace Microsoft.JavaScript.NodeApi; public class JSValueScopeClosedException : ObjectDisposedException { /// - /// Creates a new instance of with an optional + /// Creates a new instance of with an optional /// object name and message. /// public JSValueScopeClosedException(JSValueScope scope, string? message = null) diff --git a/src/NodeApi/Runtime/JSRuntime.Types.cs b/src/NodeApi/Runtime/JSRuntime.Types.cs index 6b9952c5..54d3b20e 100644 --- a/src/NodeApi/Runtime/JSRuntime.Types.cs +++ b/src/NodeApi/Runtime/JSRuntime.Types.cs @@ -114,16 +114,17 @@ public enum napi_status : int public record struct napi_callback(nint Handle) { #if UNMANAGED_DELEGATES + /// TEST TEST TEST public napi_callback( delegate* unmanaged[Cdecl] handle) : this((nint)handle) { } -#else +#endif + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate napi_value Delegate(napi_env env, napi_callback_info callbackInfo); public napi_callback(napi_callback.Delegate callback) : this(Marshal.GetFunctionPointerForDelegate(callback)) { } -#endif } public record struct napi_finalize(nint Handle) @@ -131,13 +132,13 @@ public record struct napi_finalize(nint Handle) #if UNMANAGED_DELEGATES public napi_finalize(delegate* unmanaged[Cdecl] handle) : this((nint)handle) { } -#else +#endif + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void Delegate(napi_env env, nint data, nint hint); public napi_finalize(napi_finalize.Delegate callback) - : this (Marshal.GetFunctionPointerForDelegate(callback)) { } -#endif + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } public struct napi_error_message_handler diff --git a/src/NodeApi/Runtime/JSRuntime.cs b/src/NodeApi/Runtime/JSRuntime.cs index 1cdd5eca..8177e8cc 100644 --- a/src/NodeApi/Runtime/JSRuntime.cs +++ b/src/NodeApi/Runtime/JSRuntime.cs @@ -24,8 +24,8 @@ namespace Microsoft.JavaScript.NodeApi.Runtime; /// /// Guidelines for this API: /// - Use .NET style method names, not napi_* function naming -/// - Prefer strings, Span and nint over pointers -/// - Prefer ref & out over pointers, when practical +/// - Prefer strings, Span, and nint over pointers +/// - Prefer ref and out over pointers, when practical /// - Use napi_value instead of JSValue, JSObject, JSArray, etc. /// - Do not throw exceptions; return status codes instead /// - Avoid overloads that are purely for convenience diff --git a/src/NodeApi/Runtime/NodejsEnvironment.cs b/src/NodeApi/Runtime/NodejsEnvironment.cs index aabf1fa7..1f728e9e 100644 --- a/src/NodeApi/Runtime/NodejsEnvironment.cs +++ b/src/NodeApi/Runtime/NodejsEnvironment.cs @@ -315,7 +315,7 @@ public void Post(Action action, bool allowSync = false) /// /// Runs an asynchronous action on the JS thread, without waiting for completion. /// - /// The action to run. + /// The action to run. /// True to allow the action to run immediately if the current /// synchronization context is this one. By default the action will always be scheduled /// for later execution. @@ -362,11 +362,14 @@ public Task RunAsync(Func> asyncAction) /// 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 if importing an ES module. The default is false. Note when + /// importing an ES module the returned value will be a JS Promise object that resolves to the + /// imported value. /// The imported value. - /// Both and - /// are null. - /// The function was - /// not initialized. + /// Both and + /// are null. + /// The + /// property was not initialized. public JSValue Import(string? module, string? property = null, bool esModule = false) => _scope.RuntimeContext.Import(module, property, esModule); diff --git a/src/NodeApi/Runtime/NodejsRuntime.Types.cs b/src/NodeApi/Runtime/NodejsRuntime.Types.cs index 46c390a4..a0843e19 100644 --- a/src/NodeApi/Runtime/NodejsRuntime.Types.cs +++ b/src/NodeApi/Runtime/NodejsRuntime.Types.cs @@ -12,11 +12,19 @@ public record struct napi_callback_scope(nint Handle); public record struct napi_async_context(nint Handle); public record struct napi_async_work(nint Handle); - public struct napi_cleanup_hook + public record struct napi_cleanup_hook(nint Handle) { - public delegate* unmanaged[Cdecl] Handle; - public napi_cleanup_hook(delegate* unmanaged[Cdecl] handle) - => Handle = handle; +#if UNMANAGED_DELEGATES + public napi_cleanup_hook( + delegate* unmanaged[Cdecl] handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate(nint arg); + + public napi_cleanup_hook(napi_cleanup_hook.Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } public record struct napi_threadsafe_function(nint Handle); @@ -33,36 +41,40 @@ public enum napi_threadsafe_function_call_mode : int napi_tsfn_blocking } - public struct napi_async_execute_callback + public record struct napi_async_execute_callback(nint Handle) { - public delegate* unmanaged[Cdecl] Handle; +#if UNMANAGED_DELEGATES public napi_async_execute_callback( delegate* unmanaged[Cdecl] handle) - => Handle = handle; + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate(napi_env env, void* data); + + public napi_async_execute_callback(napi_async_execute_callback.Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } - public struct napi_async_complete_callback + public record struct napi_async_complete_callback(nint Handle) { - public delegate* unmanaged[Cdecl]< - napi_env /*env*/, napi_status /*status*/, void* /*data*/, void> Handle; +#if UNMANAGED_DELEGATES public napi_async_complete_callback( delegate* unmanaged[Cdecl]< napi_env /*env*/, napi_status /*status*/, void* /*data*/, void> handle) - => Handle = handle; - } - - public struct napi_threadsafe_function_call_js - { - public nint Handle; + : this((nint)handle) { } +#endif -#if !UNMANAGED_DELEGATES [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void Delegate( - napi_env env, napi_value js_callback, nint context, nint data); + public delegate void Delegate(napi_env env, napi_status status, void* data); + + public napi_async_complete_callback(napi_async_complete_callback.Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } - public napi_threadsafe_function_call_js(Delegate callback) - => Handle = Marshal.GetFunctionPointerForDelegate(callback); -#else + public record struct napi_threadsafe_function_call_js(nint Handle) + { +#if UNMANAGED_DELEGATES public napi_threadsafe_function_call_js( delegate* unmanaged[Cdecl]< napi_env /*env*/, @@ -70,8 +82,15 @@ public napi_threadsafe_function_call_js( nint /*context*/, nint /*data*/, void> handle) - => Handle = (nint)handle; + : this((nint)handle) { } #endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate( + napi_env env, napi_value js_callback, nint context, nint data); + + public napi_threadsafe_function_call_js(napi_threadsafe_function_call_js.Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } public struct napi_node_version @@ -84,24 +103,36 @@ public struct napi_node_version public record struct napi_async_cleanup_hook_handle(nint Handle); - public struct napi_async_cleanup_hook + public record struct napi_async_cleanup_hook(nint Handle) { - public delegate* unmanaged[Cdecl]< - napi_async_cleanup_hook_handle /*handle*/, void* /*data*/, void> Handle; +#if UNMANAGED_DELEGATES public napi_async_cleanup_hook( delegate* unmanaged[Cdecl]< napi_async_cleanup_hook_handle /*handle*/, void* /*data*/, void> handle) - => Handle = handle; + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate(napi_async_cleanup_hook_handle handle, void* data); + + public napi_async_cleanup_hook(napi_async_cleanup_hook.Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } - public struct napi_addon_register_func + public record struct napi_addon_register_func(nint Handle) { - public delegate* unmanaged[Cdecl]< - napi_env /*env*/, napi_value /*exports*/, napi_value> Handle; +#if UNMANAGED_DELEGATES public napi_addon_register_func( delegate* unmanaged[Cdecl]< napi_env /*env*/, napi_value /*exports*/, napi_value> handle) - => Handle = handle; + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate napi_value Delegate(napi_env env, napi_value exports); + + public napi_addon_register_func(napi_async_cleanup_hook.Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } public struct napi_module diff --git a/src/NodeApi/Runtime/TracingJSRuntime.cs b/src/NodeApi/Runtime/TracingJSRuntime.cs index 0aead1ac..7e269cb0 100644 --- a/src/NodeApi/Runtime/TracingJSRuntime.cs +++ b/src/NodeApi/Runtime/TracingJSRuntime.cs @@ -22,22 +22,22 @@ namespace Microsoft.JavaScript.NodeApi.Runtime; /// /// Produces output similar to: /// -/// < CreateObject() -/// > CreateObject(000001E984E19BB0 object) -/// < DefineProperties(000001E984E19BB0 object, [hello(), toString()]) -/// > DefineProperties(ok) -/// < Wrap(000001E984E19BB0 object, 0x000001E984CE4580 RuntimeType) -/// > Wrap(@000001E9808E7840 000001E984E19BF0 object) -/// < DefineProperties(000001E984E19B80 object, [Example]) -/// > DefineProperties(ok) -/// < GetValueType(000001E984E19B80 object) -/// > GetValueType(object) -/// < GetInstanceData() -/// > GetInstanceData(0x000001E984CE1300 JSRuntimeContext) -/// < GetCallbackInfo(0000000CB8DFE560) -/// > GetCallbackInfo(1, 0x000001E984CE4590 JSPropertyDescriptor) -/// < GetCallbackArgs(0000000CB8DFE560, [1]) -/// > GetCallbackArgs(0000000CB8DFE8C8 object, 0000000CB8DFE8D0 string ".NET") +/// < CreateObject() +/// > CreateObject(000001E984E19BB0 object) +/// < DefineProperties(000001E984E19BB0 object, [hello(), toString()]) +/// > DefineProperties(ok) +/// < Wrap(000001E984E19BB0 object, 0x000001E984CE4580 RuntimeType) +/// > Wrap(@000001E9808E7840 000001E984E19BF0 object) +/// < DefineProperties(000001E984E19B80 object, [Example]) +/// > DefineProperties(ok) +/// < GetValueType(000001E984E19B80 object) +/// > GetValueType(object) +/// < GetInstanceData() +/// > GetInstanceData(0x000001E984CE1300 JSRuntimeContext) +/// < GetCallbackInfo(0000000CB8DFE560) +/// > GetCallbackInfo(1, 0x000001E984CE4590 JSPropertyDescriptor) +/// < GetCallbackArgs(0000000CB8DFE560, [1]) +/// > GetCallbackArgs(0000000CB8DFE8C8 object, 0000000CB8DFE8D0 string ".NET") /// /// public class TracingJSRuntime : JSRuntime @@ -375,7 +375,9 @@ private static readonly unsafe delegate* unmanaged[Cdecl] s_traceSetterCallback = &TraceSetterCallback; #endif +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value TraceFunctionCallback(napi_env env, napi_callback_info cbinfo) { using var scope = new JSValueScope(JSValueScopeType.Callback); @@ -383,7 +385,9 @@ private static unsafe napi_value TraceFunctionCallback(napi_env env, napi_callba scope, cbinfo, (descriptor) => descriptor); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value TraceMethodCallback(napi_env env, napi_callback_info cbinfo) { using var scope = new JSValueScope(JSValueScopeType.Callback); @@ -395,7 +399,9 @@ private static unsafe napi_value TraceMethodCallback(napi_env env, napi_callback propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value TraceGetterCallback(napi_env env, napi_callback_info cbinfo) { using var scope = new JSValueScope(JSValueScopeType.Callback); @@ -407,7 +413,9 @@ private static unsafe napi_value TraceGetterCallback(napi_env env, napi_callback propertyDescriptor.ModuleContext)); } +#if UNMANAGED_DELEGATES [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif private static unsafe napi_value TraceSetterCallback(napi_env env, napi_callback_info cbinfo) { using var scope = new JSValueScope(JSValueScopeType.Callback); diff --git a/src/node-api-dotnet/generator/index.js b/src/node-api-dotnet/generator/index.js index 0adbb95c..4fb163b2 100644 --- a/src/node-api-dotnet/generator/index.js +++ b/src/node-api-dotnet/generator/index.js @@ -4,7 +4,7 @@ // Licensed under the MIT License. const path = require('path'); -const assemblyDir = path.join(__dirname, 'net6.0'); +const assemblyDir = path.join(__dirname, 'net8.0'); const dotnet = require('node-api-dotnet'); diff --git a/src/node-api-dotnet/index.d.ts b/src/node-api-dotnet/index.d.ts index 402d712e..0ac68135 100644 --- a/src/node-api-dotnet/index.d.ts +++ b/src/node-api-dotnet/index.d.ts @@ -1,10 +1,38 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// APIs defined here are implemented by Microsoft.JavaScript.NodeApi.DotNetHost. - -// This explicit module declaration enables module members to be merged with imported namespaces. +/** + * Use the `node-api-dotnet` package to load .NET assemblies into a Node.js application and + * call public APIs defined in the assemblies. + * ::: code-group + * ```JavaScript [ES (TS or JS)] + * import dotnet from 'node-api-dotnet'; + * ``` + * ```TypeScript [CommonJS (TS)] + * import * as dotnet from 'node-api-dotnet'; + * ``` + * ```JavaScript [CommonJS (JS)] + * const dotnet = require('node-api-dotnet'); + * ``` + * ::: + * To load a specific version of .NET, append the target framework moniker to the package name: + * ::: code-group + * ```JavaScript [ES (TS or JS)] + * import dotnet from 'node-api-dotnet/net6.0'; + * ``` + * ```TypeScript [CommonJS (TS)] + * import * as dotnet from 'node-api-dotnet/net6.0'; + * ``` + * ```JavaScript [CommonJS (JS)] + * const dotnet = require('node-api-dotnet/net6.0'); + * ``` + * ::: + * Currently the supported target frameworks are `net472`, `net6.0`, and `net8.0`. + * @module node-api-dotnet + */ declare module 'node-api-dotnet' { +// APIs defined here are implemented by Microsoft.JavaScript.NodeApi.DotNetHost. +// The explicit module declaration enables module members to be merged with imported namespaces. /** * Gets the current .NET runtime version, for example "8.0.1". @@ -23,6 +51,8 @@ export const frameworkMoniker: string; * @param dotnetAssemblyFilePath Path to the .NET assembly DLL file. * @returns The JavaScript module exported by the assembly. (Type information for the module * may be available in a separate generated type-definitions file.) + * @description The .NET assembly must use `[JSExport]` attributes to export selected types + * and/or members to JavaScript. These exports _do not_ use .NET namespaces. */ export function require(dotnetAssemblyFilePath: string): any; @@ -31,6 +61,11 @@ export function require(dotnetAssemblyFilePath: string): any; * dynamic invocation of any APIs in the assembly. After loading, types from the assembly are * available via namespaces on the main dotnet module. * @param assemblyNameOrFilePath Path to the .NET assembly DLL file, or name of a system assembly. + * @description After loading an assembly, types in the assembly are merged into the .NET + * namespace hierarchy, with top-level namespaces available as properties on the .NET module. + * For example, if the assembly defines a type `Contoso.Business.Component`, it can be accessed as + * `dotnet.Contoso.Business.Component`. (.NET core library types can be accessed the same way, for + * example `dotnet.System.Console`.) */ export function load(assemblyNameOrFilePath: string): void; diff --git a/src/node-api-dotnet/tsconfig.json b/src/node-api-dotnet/tsconfig.json new file mode 100644 index 00000000..1ac8c10b --- /dev/null +++ b/src/node-api-dotnet/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["es2018"], + } +}