Skip to content

Commit

Permalink
Add npm package example (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin authored Nov 6, 2023
1 parent 8907200 commit 4bb12b6
Show file tree
Hide file tree
Showing 20 changed files with 189 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
out/
bin/
obj/
pkg/

*.suo
*.user
Expand Down
6 changes: 6 additions & 0 deletions examples/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<!-- False is the default, but this overrides the True value in the project root. -->
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
5 changes: 2 additions & 3 deletions examples/aot-module/aot-module.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishAot>true</PublishAot>
<PublishNodeModule>true</PublishNodeModule>
<PublishDir>bin</PublishDir>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.2.*-*" PrivateAssets="none" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.2.*-*" />
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.4.*-*" PrivateAssets="none" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.4.*-*" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions examples/aot-npm-package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

## Minimal Example .NET AOT NPM Package
The `lib/Example.cs` class defines a Node.js add-on module that is AOT-compiled, so that it does not
depend on the .NET runtime. The AOT module is then packaged as an npm package. The `app/example.js`
script loads that _native_ module via its npm package and calls a method on it. The script has
access to type definitions and doc-comments for the module's APIs via the auto-generated `.d.ts`
file that was included in the npm package.

| Command | Explanation
|-------------------------------|--------------------------------------------------
| `dotnet pack ../..` | Build Node API .NET packages.
| `cd lib`<br/>`dotnet publish` | Install Node API .NET packages into lib project; build lib project and compile to native binary; pack npm package.
| `cd app`<br/> `npm install` | Install lib project npm package into app project.
| `node example.js` | Run example JS code that calls the library API.

### Building multi-platform npm packages with platform-specific AOT binaries
Native AOT binaries are platform-specific. The `dotnet publish` command above creates a package
only for the current OS / CPU platform (aka .NET runtime-identifier). To create a multi-platform
npm package with Native AOT binaries, run `dotnet publish` separately for each runtime-identifier,
and only create the package on the last one:
```
dotnet publish -r:win-x64 -p:PackNpmPackage=false
dotnet publish -r:win-arm64 -p:PackNpmPackage=true
```

To create a fully cross-platform packatge, it will be necessary to compile on each targeted OS
(Windows, Mac, Linux), then copy the outputs into a shared directory before creating the final
npm package.
10 changes: 10 additions & 0 deletions examples/aot-npm-package/app/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const Example = require('aot-npm-package').Example;

// Call a method exported by the .NET module.
const result = Example.hello('.NET AOT');

const assert = require('assert');
assert.strictEqual(result, 'Hello .NET AOT!');
15 changes: 15 additions & 0 deletions examples/aot-npm-package/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "aot-npm-app",
"private": true,
"version": "0.1.0",
"description": "Example Node.js app that references an npm-packaged C# Native AOT node module",
"license": "MIT",
"author": "Microsoft",
"repository": "github:microsoft/node-api-dotnet",
"main": "./example.js",
"scripts": {
},
"dependencies": {
"aot-npm-package": "file:../lib/pkg/aot-npm-package-0.1.6.tgz"
}
}
6 changes: 6 additions & 0 deletions examples/aot-npm-package/lib/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# When packaging, ignore everything except the .node binaries, scripts, and type definitions.
# (Readme and license files are always included implictly.)
*
!/bin/**/*.node
!/bin/*.js
!/bin/*.d.ts
22 changes: 22 additions & 0 deletions examples/aot-npm-package/lib/Example.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.JavaScript.NodeApi.Examples;

/// <summary>
/// Example Node API module that exports a simple "hello" method.
/// </summary>
[JSExport]
public static class Example
{
/// <summary>
/// Gets a greeting string.
/// </summary>
/// <param name="greeter">Name of the greeter.</param>
/// <returns>A greeting with the name.</returns>
public static string Hello(string greeter)
{
System.Console.WriteLine($"Hello {greeter}!");
return $"Hello {greeter}!";
}
}
27 changes: 27 additions & 0 deletions examples/aot-npm-package/lib/aot-npm-package.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<!-- The C# xmldoc file is converted to comments in the generated TS type definitions. -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>

<!-- `dotnet publish` will produce node module files in $(PublishDir), with
.node native AOT binary files under $(RuntimeIdentifier) subdirectories. -->
<PublishAot>true</PublishAot>
<PublishNodeModule>true</PublishNodeModule>
<PublishDir>bin</PublishDir>
<PublishMultiPlatformNodeModule>true</PublishMultiPlatformNodeModule>

<!-- `dotnet publish` will produce an npm package in the $(PackageOutputPath) directory. -->
<!-- `package.json` is required in the project directory; `.npmignore` is also recommended. -->
<PackNpmPackage>true</PackNpmPackage>
<PackageOutputPath>pkg</PackageOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.4.*-*" PrivateAssets="none" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.4.*-*" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions examples/aot-npm-package/lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "aot-npm-package",
"private": true,
"version": "0.1.6",
"description": "Example npm-packaged C# Native AOT node module",
"license": "MIT",
"author": "Microsoft",
"repository": "github:microsoft/node-api-dotnet",
"main": "./bin/aot-npm-package",
"scripts": {
}
}
5 changes: 2 additions & 3 deletions examples/dotnet-module/dotnet-module.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<OutDir>bin</OutDir>
<LangVersion>10</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.2.*-*" PrivateAssets="all" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.2.*-*" />
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.4.*-*" PrivateAssets="all" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.4.*-*" />
</ItemGroup>

</Project>
1 change: 0 additions & 1 deletion examples/hermes-engine/hermes-engine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Hermes.Example</RootNamespace>
Expand Down
3 changes: 2 additions & 1 deletion examples/semantic-kernel/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
// @ts-check

import dotnet from 'node-api-dotnet';
import './bin/Microsoft.Extensions.Logging.Abstractions.js';
import './bin/Microsoft.SemanticKernel.Core.js';
import './bin/Microsoft.SemanticKernel.Functions.Semantic.js';
import './bin/Microsoft.SemanticKernel.Connectors.AI.OpenAI.js';

const SK = dotnet.Microsoft.SemanticKernel;
const Logging = dotnet.Microsoft.Extensions.Logging;
const SK = dotnet.Microsoft.SemanticKernel;

/** @type {dotnet.Microsoft.Extensions.Logging.ILogger} */
const logger = {
Expand Down
3 changes: 3 additions & 0 deletions examples/semantic-kernel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
"type": "module",
"dependencies": {
"node-api-dotnet": "file:../../out/pkg/node-api-dotnet"
},
"devDependencies": {
"@types/node": "^20.8.10"
}
}
1 change: 0 additions & 1 deletion examples/semantic-kernel/semantic-kernel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<RestorePackagesPath>$(MSBuildThisFileDirectory)/pkg</RestorePackagesPath>
<OutDir>bin</OutDir>
<NodeApiAssemblyJSModuleType>esm</NodeApiAssemblyJSModuleType>
Expand Down
1 change: 0 additions & 1 deletion examples/winui-fluid/winui-fluid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<TargetFramework>net8.0-windows10.0.22000.0</TargetFramework>
<TargetPlatformMinVersion>10.0.22000.0</TargetPlatformMinVersion>
<OutputType>WinExe</OutputType>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>

<!-- WinUI 3 -->
<UseWinUI>true</UseWinUI>
Expand Down
1 change: 0 additions & 1 deletion examples/wpf/WpfExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<RestorePackagesPath>$(MSBuildThisFileDirectory)/pkg</RestorePackagesPath>
<OutDir>bin</OutDir>
<NodeApiAssemblyJSModuleType>esm</NodeApiAssemblyJSModuleType>
Expand Down
12 changes: 12 additions & 0 deletions src/NodeApi.Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
_autoCamelCase = autoCamelCase ?? !_exportAll;

s++;

// Declare this types as members of the 'node-api-dotnet' module.
// This causes types across multiple .NET assemblies to be merged into
// a shared .NET namespace hierarchy.
s += "declare module 'node-api-dotnet' {";

foreach (Type type in _assembly.GetTypes().Where((t) => t.IsPublic))
Expand Down Expand Up @@ -316,6 +320,14 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
s.Insert(importsIndex, insertBuilder.ToString());
}

// Re-export this module's types in a module that matches the assembly name.
// This supports AOT when the module is directly imported by name instead of
// importing via the .NET host.
s++;
s += $"declare module '{_assembly.GetName().Name}' {{";
s += "export * from 'node-api-dotnet';";
s += "}";

return s;
}

Expand Down
37 changes: 33 additions & 4 deletions src/NodeApi/NodeApi.targets
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,43 @@
BeforeTargets="PublishManagedAssembly"
Condition=" '$(PublishNodeModule)' == 'true' "
>
<!-- Rename the native library (and its symbols file) to have a .node extension. -->
<!-- If publishing a node module for for multiple platforms, use an rid-specific subdir. -->
<PropertyGroup>
<PublishNodeModuleDir Condition=" '$(PublishMultiPlatformNodeModule)' != 'true' ">$(PublishDir)</PublishNodeModuleDir>
<PublishNodeModuleDir Condition=" '$(PublishMultiPlatformNodeModule)' == 'true' ">$(PublishDir)$(RuntimeIdentifier)/</PublishNodeModuleDir>
</PropertyGroup>
<MakeDir Directories="$(PublishNodeModuleDir)" />

<!-- Rename/move the native library (and its symbols file) to have a .node extension. -->
<Move SourceFiles="$(PublishDir)$(TargetName)$(NativeBinaryExt)"
DestinationFiles="$(PublishDir)$(TargetName).node" />
DestinationFiles="$(PublishNodeModuleDir)$(TargetName).node" />
<Move Condition="Exists('$(PublishDir)$(TargetName).pdb')"
SourceFiles="$(PublishDir)$(TargetName).pdb"
DestinationFiles="$(PublishDir)$(TargetName).node.pdb" />
DestinationFiles="$(PublishNodeModuleDir)$(TargetName).node.pdb" />
<Move Condition="Exists('$(PublishDir)$(TargetName).so.dbg')"
SourceFiles="$(PublishDir)$(TargetName).so.dbg"
DestinationFiles="$(PublishDir)$(TargetName).node.dbg" />
DestinationFiles="$(PublishNodeModuleDir)$(TargetName).node.dbg" />

<!-- Add a non-rid-specific JS file that redirects to the rid-specific binary. -->
<!-- (The rid code is the same as node-api-dotnet/init.js.) -->
<WriteLinesToFile Condition=" '$(PublishMultiPlatformNodeModule)' == 'true' "
File="$(PublishDir)$(TargetName).js" Overwrite="true" Lines=";
const ridPlatform =
process.platform === 'win32' ? 'win' :
process.platform === 'darwin' ? 'osx' :
process.platform;
const ridArch = process.arch === 'ia32' ? 'x86' : process.arch;
const rid = `${ridPlatform}-${ridArch}`;
module.exports = require(`./${rid}/$(TargetName).node`);
" />
</Target>

<Target Name="PackNpmPackage"
AfterTargets="RenameToDotNode"
BeforeTargets="PublishManagedAssembly"
Condition=" '$(PackNpmPackage)' == 'true' "
>
<MakeDir Directories="$(PackageOutputPath)" />
<Exec Command="npm pack --pack-destination=&quot;$(PackageOutputPath)&quot;" />
</Target>
</Project>
29 changes: 8 additions & 21 deletions src/node-api-dotnet/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
// - ...
module.exports = initialize;

const ridPlatform =
process.platform === 'win32' ? 'win' :
process.platform === 'darwin' ? 'osx' :
process.platform;
const ridArch = process.arch === 'ia32' ? 'x86' : process.arch;
const rid = `${ridPlatform}-${ridArch}`;

/**
* Initializes the Node API .NET host.
* @param {string} targetFramework Minimum requested .NET version. Must be one of the target
Expand All @@ -18,29 +25,9 @@ module.exports = initialize;
*/
function initialize(targetFramework) {
const assemblyName = 'Microsoft.JavaScript.NodeApi';
const runtimeIdentifier = getRuntimeIdentifier();
const nativeHostPath = __dirname + `/${runtimeIdentifier}/${assemblyName}.node`;
const nativeHostPath = __dirname + `/${rid}/${assemblyName}.node`;
const managedHostPath = __dirname + `/${targetFramework}/${assemblyName}.DotNetHost.dll`

const nativeHost = require(nativeHostPath);
return nativeHost.initialize(targetFramework, managedHostPath, require);
}

function getRuntimeIdentifier() {
function getRuntimePlatformIdentifier() {
switch (process.platform) {
case 'win32': return 'win'
case 'darwin': return 'osx'
default: return process.platform;
}
}

function getRuntimeArchIdentifier() {
switch (process.arch) {
case 'ia32': return 'x86'
default: return process.arch
}
}

return `${getRuntimePlatformIdentifier()}-${getRuntimeArchIdentifier()}`;
}

0 comments on commit 4bb12b6

Please sign in to comment.