microsoft / microsoft/node-api-dotnet

A possible vite plugin to make this work with electron.

Open
#397 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
783
Forks
80
PR merge metrics
No merged PRs in 30d

Description

After days of research I wrote a plugin to make this cool node-api-dotnet works with electron. Hope it can help people later.

  1. Create a vite plugin
import fs from "fs";
import path from "path";

interface ElectronNodeApiDotNetOptions {
    // The path to `node_modules/node-api-dotnet
    nodeModulePath: string,

    // The path to all generated files and assemblies
    csprojOutputPath: string,
}

export default (options: ElectronNodeApiDotNetOptions) => {
    const name = "electron-node-api-dotnet";

    let assemblyName = "Microsoft.JavaScript.NodeApi";
    let assemblyRoot = path.normalize(options.csprojOutputPath)

    const transformInitJs = (code: string) => {

        // Create require to dynamic require .node files.
        code = `
                    import path from "path";
                    import { createRequire } from 'module';
                    const require = createRequire(import.meta.url);
                    const bundleRoot = ${JSON.stringify(options.nodeModulePath)};
                ` + code

        // Since we use import above, we also need to replace the export style.
        // https://github.com/rollup/plugins/issues/1121
        code = code.replace(
            "module.exports = initialize",
            "export default initialize"
        )

        // Get the dotnet assembly name, preventing upstream change.
        const regex = /const assemblyName\s*=\s*['"`](.*?)['"`];/;
        const match = code.match(regex);
        if (match) {
            assemblyName = match[1];
            console.log(`[${name}] Using assembly name: ${assemblyName}`);
        } else {
            console.error(
                `[${name}] Cannot match assembly name, plugin may not work!`
            );
        }

        // Transform dynamic assemblyName require to a static one
        // Also transform the relative paths to be prefixed with provided bundle root.
        code = code.replace(
            /require\(`\.\/(.+?)\/\$\{assemblyName\}\.node`\)/g,
            "require(path.join(bundleRoot, `./$1/" + assemblyName + ".node`))"
        );

        // Transform the default case.
        code = code.replace(
            "nativeHost = require(__dirname + `/${rid}/${assemblyName}.node`)",
            "nativeHost = require(path.join(bundleRoot, `./${rid}/${assemblyName}.node`))"
        )

        // Transform the path for managed host.
        code = code.replace(
            "const managedHostPath = __dirname + `/${targetFramework}/${assemblyName}.DotNetHost.dll`",
            "const managedHostPath = path.join(bundleRoot, `./${targetFramework}/${assemblyName}.DotNetHost.dll`)"
        )

        return code;
    }

    const transformAssemblyJs = (code: string, filePath: string) => {

        // Instead of use import.meta.url
        code = code.replace(
            "import { fileURLToPath } from 'node:url';", ""
        )
        
        // Use the absolute path, because these files will be rollup 
        // to one single file (for example main.js)
        code = code.replace(
            "const __filename = fileURLToPath(import.meta.url)",
            `const __filename = ${JSON.stringify(filePath)}`
        )

        return code
    }

    return {
        name,
        resolveId(source: string, importer: string | undefined, options: any) {
            return null;
        },
        transform(code: string, id: string) {
            const normId = path.normalize(id)
            if (id.endsWith("node_modules/node-api-dotnet/init.js")) {
                return transformInitJs(code)
            } else if (normId.startsWith(assemblyRoot)) {
                return transformAssemblyJs(code, normId)
            }
            return code;
        },
        load(id: string) {
            if (id.endsWith(`${assemblyName}.node`)) {
                // Treat .node files as empty string, otherwise there'll be error because binary are
                // not valid js files.
                return ``;
            }
            return null;
        },
        generateBundle(options: any, bundle: any) {
            // bundle whatever way you want
        },
    };
};

  1. Include it in vite config, or nuxt config, if you like. Remember to exclude files from commonjs plugin.
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import electronNodeDotNetApi from './rollup/electron-node-api-dotnet'

...

vite: {
  plugins: [
    resolve(),
    commonjs({
      exclude: [
        "node_modules/node-api-dotnet/**"
      ]
    }),
    electronNodeDotNetApi({
      nodeModulePath: path.join(projectRootDir, 'node_modules/node-api-dotnet'),
      csprojOutputPath: csharpOutputDir
    }),
  ]
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review the proposed Vite plugin code and the Vite configuration example, including its transforms for node-api-dotnet/init.js and generated assembly files. First clarify whether this project wants to maintain Electron/Vite integration and what files or tests should be added; done requires agreed acceptance criteria because the issue does not define them.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, electron, node.js, typescript, vite
Domain
build-system, desktop, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.