Skip to content

Commit

Permalink
Specify target framework in generated assembly loader script (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin authored Oct 26, 2024
1 parent 5fb0a61 commit b0e115f
Show file tree
Hide file tree
Showing 26 changed files with 100 additions and 193 deletions.
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

0 comments on commit b0e115f

Please sign in to comment.