native-modules-plugin

API documentation for the native modules plugin

native-modules-plugin

Handles native Node.js addons (.node files) by copying them to the output directory and generating appropriate import statements.

Installation

The plugin is included with @visulima/packem-rollup:

npm install @visulima/packem

Usage

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  transformer,
  rollup: {
    nativeModules: {
      nativesDirectory: 'natives'
    }
  }
})

API Reference

nativeModulesPlugin

function nativeModulesPlugin(
  options?: NativeModulesOptions
): Plugin

Creates a Rollup plugin that handles native Node.js addons.

Parameters

  • options (NativeModulesOptions?): Plugin configuration options

Returns

A Rollup plugin instance.

Options

NativeModulesOptions

interface NativeModulesOptions {
  /**
   * Custom subdirectory name for native modules within the output directory
   * @default 'natives'
   */
  nativesDirectory?: string
}

nativesDirectory

Type: string
Default: 'natives'

The subdirectory name within the output directory where native modules will be copied. This directory will be created automatically.

Example:

nativeModules: {
  nativesDirectory: 'native-addons'
}

How It Works

The native modules plugin operates in two stages:

Stage 1: Resolve and Load (Build Time)

  1. Detection: Identifies .node file imports in your code
  2. Resolution: Resolves the file paths and validates accessibility
  3. Registration: Registers native modules for copying
  4. Virtual Module: Creates virtual module IDs for each native module

Stage 2: Bundle Generation (Bundle Time)

  1. Directory Creation: Creates the natives directory in the output
  2. File Copying: Copies all registered .node files in parallel
  3. Import Generation: Generates appropriate require() statements

Examples

Basic Usage

// src/index.ts
import myAddon from './my-addon.node'

export function useAddon() {
  return myAddon.someFunction()
}

Output:

// dist/index.js
const myAddon = require("./natives/my-addon.node")

export function useAddon() {
  return myAddon.someFunction()
}

Custom Directory

export default defineConfig({
  transformer,
  rollup: {
    nativeModules: {
      nativesDirectory: 'addons'
    }
  }
})

Output:

const myAddon = require("./addons/my-addon.node")

Multiple Native Modules

// src/index.ts
import addon1 from './addon1.node'
import addon2 from './addon2.node'
import addon3 from './addon3.node'

export { addon1, addon2, addon3 }

Output:

const addon1 = require("./natives/addon1.node")
const addon2 = require("./natives/addon2.node")
const addon3 = require("./natives/addon3.node")

export { addon1, addon2, addon3 }

File Structure

project/
├── src/
│   ├── index.ts
│   └── my-addon.node
├── dist/
│   ├── index.js
│   └── natives/
│       └── my-addon.node  # Copied here
└── package.json

Import Patterns

The plugin supports various import patterns for native modules:

Default Import

import myAddon from './my-addon.node'

Named Import

import { someFunction } from './my-addon.node'

Dynamic Import

const myAddon = await import('./my-addon.node')

Error Handling

The plugin provides clear error messages for common issues:

File Not Found

Native module not found: /path/to/missing.node

Output Directory Not Configured

Output directory not detected. Please ensure Rollup output options are configured.

Module Registration Failed

Could not find staged native module for: /path/to/module.node

Platform Considerations

Cross-Platform Builds

Native modules are platform-specific. When building for multiple platforms:

  1. Separate builds: Create separate builds for each target platform
  2. Conditional loading: Use dynamic imports with platform detection
  3. Optional dependencies: Mark platform-specific modules as optional

Platform-Specific Imports

// src/native.ts
const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin'
const isLinux = process.platform === 'linux'

let nativeModule
if (isWindows) {
  nativeModule = await import('./native.win32.node')
} else if (isMac) {
  nativeModule = await import('./native.darwin.node')
} else if (isLinux) {
  nativeModule = await import('./native.linux.node')
}

export default nativeModule

Performance Considerations

Bundle Size

  • Native modules are copied as-is to the output directory
  • File size impacts distribution size but not bundle parsing time
  • Consider compression for large native modules

Load Time

  • Native modules load faster than JavaScript bundles
  • No JavaScript parsing overhead for native code
  • Platform-specific optimization opportunities

Troubleshooting

Module Not Copied

  1. Verify import syntax: Use relative imports for .node files
  2. Check file existence: Ensure the .node file exists at build time
  3. Validate path resolution: Confirm relative paths are correct

Import Errors

  1. Output directory: Ensure the output directory is properly configured
  2. File permissions: Check read/write permissions for native module files
  3. Platform compatibility: Verify the native module matches the target platform

Build Failures

  1. Clean build: Remove dist/ and rebuild
  2. Dependency order: Ensure native modules are built before bundling
  3. File watchers: Restart file watchers after adding new native modules

Integration Examples

With Electron

// electron/main.ts
import { app, BrowserWindow } from 'electron'
import nativeAddon from './native-addon.node'

app.whenReady().then(() => {
  // Use native addon in main process
  const result = nativeAddon.initialize()
  
  const mainWindow = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true
    }
  })
})

With N-API

// C++ addon with N-API
#include <napi.h>

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("version", Napi::String::New(env, "1.0.0"));
  return exports;
}

NODE_API_MODULE(addon, Init)
// TypeScript usage
import addon from './addon.node'

console.log('Addon version:', addon.version)
  • defineConfig - Main configuration function
  • PackemConfig - Configuration interface
  • PackemRollupOptions - Rollup configuration options

See Also

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues