Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify target framework in generated assembly loader script #394

Merged
merged 2 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/msbuild-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The following properties can be used to customize the build processes for genera
| `NodeApiTypeDefinitionsFileName` | Name of the type-definitions file generated for a project. Defaults to `$(TargetName).d.ts`. |
| `NodeApiTypeDefinitionsEnableWarnings` | Set to `true` to enable warnings when [generating type definitions](../features/type-definitions). The warnings are suppressed by default because they can be verbose when referencing system or external assemblies.
| `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. |
| `NodeApiTargetFramework` | Target framework moniker that will be loaded at runtime for the Node API .NET host. Default is the project's `$(TargetFramework)`.
| `NodeApiSystemReferenceAssembly` | Item-list of assembly names (not file paths) to be included in typedefs generator. The `System`, `System.Runtime`, and `System.Console` assemblies are included by default. Add system assembly names to the item-list to generate type definitions for them. System assemblies are provided by the installed .NET SDK. |
| `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. |
Expand Down
4 changes: 3 additions & 1 deletion src/NodeApi.Generator/NodeApi.Generator.targets
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
<NodeApiJSModuleType Condition=" '$(NodeApiJSModuleType)' == '' AND Exists('$(NodeApiPackageJson)') ">&quot;$(NodeApiPackageJson)&quot;</NodeApiJSModuleType>
<NodeApiJSModuleType Condition=" '$(NodeApiJSModuleType)' == '' ">commonjs,esm</NodeApiJSModuleType>

<NodeApiTypeDefinitionsGeneratorOptions>--module $(NodeApiJSModuleType) --framework $(TargetFramework) $(NodeApiTypedefsGeneratorOptions)</NodeApiTypeDefinitionsGeneratorOptions>
<NodeApiTargetFramework Condition=" '$(NodeApiTargetFramework)' == '' ">$(TargetFramework)</NodeApiTargetFramework>

<NodeApiTypeDefinitionsGeneratorOptions>--module $(NodeApiJSModuleType) --framework $(NodeApiTargetFramework) $(NodeApiTypedefsGeneratorOptions)</NodeApiTypeDefinitionsGeneratorOptions>
</PropertyGroup>

<!--
Expand Down
24 changes: 12 additions & 12 deletions src/NodeApi.Generator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static class Program
{
private const char PathSeparator = ';';

private static string? s_targetFramework;
private static readonly List<string> s_assemblyPaths = new();
private static readonly List<string> s_referenceAssemblyDirectories = new();
private static readonly List<string> s_referenceAssemblyPaths = new();
Expand All @@ -49,7 +50,7 @@ public static int Main(string[] args)
Console.WriteLine("""
Usage: node-api-dotnet-generator [options...]
-a --asssembly Path to input assembly (required)
-f --framework Target framework of system assemblies (optional)
-f --framework .NET target framework moniker (optional)
-p --pack Targeting pack (optional, multiple)
-r --reference Path to reference assembly (optional, multiple)
-t --typedefs Path to output type definitions file (required)
Expand Down Expand Up @@ -105,6 +106,7 @@ public static int Main(string[] args)
s_referenceAssemblyDirectories,
s_typeDefinitionsPaths[i],
modulePaths,
s_targetFramework,
isSystemAssembly: s_systemAssemblyIndexes.Contains(i),
s_suppressWarnings);
}
Expand All @@ -119,7 +121,6 @@ private static bool ParseArgs(string[] args)
return false;
}

string? targetFramework = null;
List<string> targetingPacks = new();

for (int i = 0; i < args.Length; i++)
Expand Down Expand Up @@ -149,7 +150,7 @@ void AddItems(List<string> list, string items)

case "-f":
case "--framework":
targetFramework = args[++i];
s_targetFramework = args[++i];
break;

case "-p":
Expand Down Expand Up @@ -220,7 +221,7 @@ void AddItems(List<string> list, string items)
}
}

ResolveSystemAssemblies(targetFramework, targetingPacks);
ResolveSystemAssemblies(targetingPacks);

bool HasAssemblyExtension(string fileName) =>
fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
Expand Down Expand Up @@ -396,20 +397,19 @@ private static IEnumerable<string> SplitWithQuotes(string line)
}

private static void ResolveSystemAssemblies(
string? targetFramework,
List<string> targetingPacks)
{
if (targetFramework == null)
if (s_targetFramework == null)
{
targetFramework = GetCurrentFrameworkTarget();
s_targetFramework = GetCurrentFrameworkTarget();
}
else if (targetFramework.Contains('-'))
else if (s_targetFramework.Contains('-'))
{
// Strip off a platform suffix from a target framework like "net6.0-windows".
targetFramework = targetFramework.Substring(0, targetFramework.IndexOf('-'));
s_targetFramework = s_targetFramework.Substring(0, s_targetFramework.IndexOf('-'));
}

if (targetFramework.StartsWith("net4"))
if (s_targetFramework.StartsWith("net4"))
{
if (targetingPacks.Count > 0)
{
Expand All @@ -422,7 +422,7 @@ private static void ResolveSystemAssemblies(
"Microsoft",
"Framework",
".NETFramework",
"v" + string.Join(".", targetFramework.Substring(3).ToArray())); // v4.7.2
"v" + string.Join(".", s_targetFramework.Substring(3).ToArray())); // v4.7.2
if (Directory.Exists(refAssemblyDirectory))
{
s_referenceAssemblyDirectories.Add(refAssemblyDirectory);
Expand Down Expand Up @@ -461,7 +461,7 @@ private static void ResolveSystemAssemblies(
{
string? refAssemblyDirectory = Directory.GetDirectories(targetPackDirectory)
.OrderByDescending((d) => Path.GetFileName(d))
.Select((d) => Path.Combine(d, "ref", targetFramework))
.Select((d) => Path.Combine(d, "ref", s_targetFramework))
.FirstOrDefault(Directory.Exists);
if (refAssemblyDirectory != null)
{
Expand Down
144 changes: 82 additions & 62 deletions src/NodeApi.Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public enum ModuleType
ES,
}

private enum AssemblyType
{
JSModule,
ApplicationAssembly,
SystemAssembly,
}

/// <summary>
/// JavaScript (not TypeScript) code that is emitted to a `.js` file alongside the `.d.ts`.
/// Enables application code to load an assembly (containing explicit JS exports) as an ES
Expand All @@ -58,7 +65,7 @@ public enum ModuleType
/// An MSBuild task during the AOT publish process sets the `dotnet` variable to undefined.
/// </remarks>
private const string LoadModuleMJS = @"
import dotnet from 'node-api-dotnet';
import dotnet from 'node-api-dotnet/$(TargetFramework)';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
// @ts-ignore - https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65252
Expand Down Expand Up @@ -92,7 +99,7 @@ function importAotModule(moduleName) {
/// An MSBuild task during the AOT publish process sets the `dotnet` variable to undefined.
/// </remarks>
private const string LoadModuleCJS = @"
const dotnet = require('node-api-dotnet');
const dotnet = require('node-api-dotnet/$(TargetFramework)');
const path = require('node:path');
// @ts-ignore - https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65252
const { dlopen, platform, arch } = require('node:process');
Expand Down Expand Up @@ -126,7 +133,7 @@ function importAotModule(moduleName) {
/// they are equivalent to those predefined values defined for CommonJS modules.
/// </remarks>
private const string LoadAssemblyMJS = @"
import dotnet from 'node-api-dotnet';
import dotnet from 'node-api-dotnet/$(TargetFramework)';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

Expand All @@ -143,7 +150,7 @@ function importAotModule(moduleName) {
/// the node-api-dotnet module.
/// </summary>
private const string LoadAssemblyCJS = @"
const dotnet = require('node-api-dotnet');
const dotnet = require('node-api-dotnet/$(TargetFramework)');
const path = require('node:path');

const assemblyName = path.basename(__filename, __filename.match(/(\.[cm]?js)?$/)[0]);
Expand All @@ -157,7 +164,7 @@ function importAotModule(moduleName) {
/// the node-api-dotnet module.
/// </summary>
private const string LoadSystemAssemblyMJS = @"
import dotnet from 'node-api-dotnet';
import dotnet from 'node-api-dotnet/$(TargetFramework)';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

Expand All @@ -172,7 +179,7 @@ function importAotModule(moduleName) {
/// it augments the node-api-dotnet module.
/// </summary>
private const string LoadSystemAssemblyCJS = @"
const dotnet = require('node-api-dotnet');
const dotnet = require('node-api-dotnet/$(TargetFramework)');
const path = require('node:path');

const assemblyName = path.basename(__filename, __filename.match(/(\.[cm]?js)?$/)[0]);
Expand Down Expand Up @@ -209,6 +216,7 @@ public static void GenerateTypeDefinitions(
IEnumerable<string> systemReferenceAssemblyDirectories,
string typeDefinitionsPath,
IDictionary<ModuleType, string> modulePaths,
string? targetFramework = null,
bool isSystemAssembly = false,
bool suppressWarnings = false)
{
Expand Down Expand Up @@ -286,7 +294,7 @@ public static void GenerateTypeDefinitions(
string moduleFilePath = moduleTypeAndPath.Value;

SourceText generatedModule = generator.GenerateModuleLoader(
moduleType, isSystemAssembly);
moduleType, targetFramework, isSystemAssembly);
File.WriteAllText(moduleFilePath, generatedModule.ToString());
}
}
Expand Down Expand Up @@ -466,82 +474,94 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
return s;
}

public SourceText GenerateModuleLoader(ModuleType moduleType, bool isSystemAssembly = false)
public SourceText GenerateModuleLoader(
ModuleType moduleType,
string? targetFramework,
bool isSystemAssembly = false)
{
var s = new SourceBuilder();
s += GetGeneratedFileHeader();

if (_isModule)
s += GetLoaderJS(
moduleType,
_isModule ? AssemblyType.JSModule :
isSystemAssembly ? AssemblyType.SystemAssembly : AssemblyType.ApplicationAssembly,
targetFramework);

if (_isModule && moduleType == ModuleType.ES)
{
if (moduleType == ModuleType.ES)
// Declare ES module exports.

bool isFirstMember = true;
bool hasDefaultExport = false;
foreach (MemberInfo member in _exportedMembers)
{
s += LoadModuleMJS.Replace(" ", ""); // The SourceBuilder will auto-indent.
string exportName = GetExportName(member);

bool isFirstMember = true;
bool hasDefaultExport = false;
foreach (MemberInfo member in _exportedMembers)
if (member is PropertyInfo exportedProperty &&
exportedProperty.SetMethod != null)
{
string exportName = GetExportName(member);

if (member is PropertyInfo exportedProperty &&
exportedProperty.SetMethod != null)
{
ReportWarning(
DiagnosticId.ESModulePropertiesAreConst,
$"Module-level property '{exportName}' with setter will be " +
"exported as read-only because ES module properties are constant.");
}
ReportWarning(
DiagnosticId.ESModulePropertiesAreConst,
$"Module-level property '{exportName}' with setter will be " +
"exported as read-only because ES module properties are constant.");
}

if (exportName == "default")
if (exportName == "default")
{
hasDefaultExport = true;
}
else
{
if (isFirstMember)
{
hasDefaultExport = true;
s++;
isFirstMember = false;
}
else
{
if (isFirstMember)
{
s++;
isFirstMember = false;
}

s += $"export const {exportName} = exports.{exportName};";
}
}

if (hasDefaultExport)
{
s++;
s += $"export default exports['default'];";
s += $"export const {exportName} = exports.{exportName};";
}
}
else if (moduleType == ModuleType.CommonJS)
{
s += LoadModuleCJS.Replace(" ", ""); // The SourceBuilder will auto-indent.
}
else

if (hasDefaultExport)
{
throw new ArgumentException(
"Invalid module type: " + moduleType, nameof(moduleType));
s++;
s += $"export default exports['default'];";
}
}

return s;
}

private static string GetLoaderJS(
ModuleType moduleType,
AssemblyType assemblyType,
string? targetFramework)
{
string loaderJS = (moduleType, assemblyType) switch
{
(ModuleType.CommonJS, AssemblyType.JSModule) => LoadModuleCJS,
(ModuleType.ES, AssemblyType.JSModule) => LoadModuleMJS,
(ModuleType.CommonJS, AssemblyType.ApplicationAssembly) => LoadAssemblyCJS,
(ModuleType.ES, AssemblyType.ApplicationAssembly) => LoadAssemblyMJS,
(ModuleType.CommonJS, AssemblyType.SystemAssembly) => LoadSystemAssemblyCJS,
(ModuleType.ES, AssemblyType.SystemAssembly) => LoadSystemAssemblyMJS,
_ => throw new ArgumentException(
"Invalid module type: " + moduleType, nameof(moduleType)),
};

loaderJS = loaderJS.Replace(" ", ""); // The SourceBuilder will auto-indent.

if (string.IsNullOrEmpty(targetFramework))
{
loaderJS = loaderJS.Replace("/$(TargetFramework)", string.Empty);
}
else
{
if (moduleType == ModuleType.ES)
{
s += isSystemAssembly ? LoadSystemAssemblyMJS : LoadAssemblyMJS;
}
else if (moduleType == ModuleType.CommonJS)
{
s += isSystemAssembly ? LoadSystemAssemblyCJS : LoadAssemblyCJS;
}
else
{
throw new ArgumentException(
"Invalid module type: " + moduleType, nameof(moduleType));
}
loaderJS = loaderJS.Replace("$(TargetFramework)", targetFramework);
}

return s;
return loaderJS;
}

private bool IsExported(MemberInfo member)
Expand Down
1 change: 0 additions & 1 deletion src/NodeApi/JSValueScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ public JSValueScope(
runtime ??= _parentScope?.Runtime ??
throw new ArgumentNullException(nameof(runtime), "A runtime is required.");

_parentScope = null;
_env = env;
ThreadId = Environment.CurrentManagedThreadId;
Runtime = runtime;
Expand Down
3 changes: 2 additions & 1 deletion test/JSProjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public class JSProjectTests

public static IEnumerable<object[]> TestCases { get; } = ListTestCases(
(testCaseName) => testCaseName.StartsWith("projects/") &&
IsCurrentTargetFramework(Path.GetFileName(testCaseName)));
(!testCaseName.Contains("-dynamic") ||
IsCurrentTargetFramework(Path.GetFileName(testCaseName))));

private static bool IsCurrentTargetFramework(string target)
{
Expand Down
9 changes: 0 additions & 9 deletions test/TestCases/projects/js-cjs-dynamic/default.js

This file was deleted.

5 changes: 0 additions & 5 deletions test/TestCases/projects/js-cjs-module/default.js

This file was deleted.

5 changes: 0 additions & 5 deletions test/TestCases/projects/js-cjs-module/net472.js

This file was deleted.

5 changes: 0 additions & 5 deletions test/TestCases/projects/js-cjs-module/net6.0.js

This file was deleted.

Loading
Loading