napi 0.15.0
napi: ^0.15.0 copied to clipboard
Generate Dart bindings for Node-API addons and WebAssembly modules.
napi #
A Dart binding compiler for native Node-API .node addons and Dart Wasm.
Declare an API in Dart, build an npm package, and import it normally:
// lib/counter.dart
import 'package:napi/napi.dart';
@NAPI()
class Counter {
int value;
Counter([this.value = 1]);
Counter.seed(this.value);
int increment([int amount = 1]) => value += amount;
}
import { Counter } from '@scope/counter';
const counter = new Counter();
counter.increment(); // 2, executed in Dart
@scope/counter is the example package built below. Imports initialize the
selected backend; consumers do not register bindings or call application main.
The product follows napi-rs's model: one Dart export declaration, generated
bindings and TypeScript types, and npm packages for native and Wasm targets.
Generated JavaScript loaders and runtime helpers initialize bindings on import.
Both targets preserve the same supported calls, including new Counter() and
argument presence. The experimental native target generates scalar, structural, value-class,
reference-class, resource, Future, Stream and retained/async-callback AOT bindings
for macOS arm64; see native builds for runtime downloads and offline builds.
See the architecture for backend boundaries and
compatibility for supported toolchains.
Supported bindings #
- Resolved
@NAPI()discovery with renaming, class/member selection and reexports - External selections for unmodified third-party libraries, sharing annotation metadata
- Required positional scalar functions:
bool,String,double,int,BigInt, and nullable forms - Optional positional and named scalar arguments with resolved Dart defaults and exact presence capture
- Nested Lists, Maps, explicit Dictionaries and records with strict fields, reference/opaque value leaves and bounded traversal on both targets (opaque Map keys remain unsupported)
- Independent Uint8List/ByteBuffer snapshots with strict brands, visible byte ranges and invalidation checks on both targets
- Explicit SharedBytes live views with checked resizable windows, weak identity and bulk operations on both targets
- JsArray/JsMap handles with scalar or direct opaque payloads, retaining the original JavaScript container even inside copied structures and value classes
- Safe-number
intby default, or fixed JavaScriptbigintwith@NAPI(bigint: true) - A pinned Dart SDK adapter and linker producing actual Wasm named exports
- Generated reference classes, named/factory constructors and weak wrapper identity on both targets
- Generic payload storage and instance/static generic methods, including inherited instance methods, with identity, nullish presence and shared TypeScript relationships
- Finite concrete reference, resource and value aliases with real Dart type arguments, nested substitutions and policy-specific representations
- Structural value classes with explicit constructors, recursive fields and direction-specific TypeScript
- Future-returning functions/methods/getters, including generic payloads, with standard Promise resolution and shared error conversion
- Resource cleanup with shared state, stable completion identity and operation leases through Promise settlement
- Typed callbacks with scalar/List/record/JS-handle conversions, weak adapter identity, retained ownership and Promise results
- Stream async iterators with FIFO reads, bounded snapshots, cancellation and resource cleanup integration
- Explicit AbortSignal inputs with cooperative Future cancellation and once-only Stream subscription cancellation
- Fresh single- or dual-target npm packages with explicit entries, assets and atomic no-replace publication
- Precompiled native bridge/runtime; business builds use only the ordinary Dart SDK
Wasm uses generated JavaScript dependencies; native uses an automatic ESM entry.
buildPackage defaults to Wasm. When both targets are included, target selects
its root at build time; /native and /wasm select a backend explicitly. There
is no automatic runtime selection or implicit fallback.
Build-time checks reject unsupported signatures before writing output. See annotation syntax and scope and the scalar conversion contract, argument presence and defaults, structural values, JS container handles and collection ownership, binary snapshots, shared byte views, value classes, reference classes, opaque payloads, concrete class aliases, resource cleanup, Future exports, callbacks, streams, explicit cancellation, and npm package assembly.
Build a package #
Annotated sources import package:napi/napi.dart. Use a separate Dart VM script
to build the example package:
// tool/build.dart
import 'dart:io';
import 'package:napi/build.dart';
Future<void> main() async {
await buildPackage(
library: File('lib/counter.dart').absolute.uri,
output: Directory('npm-package'),
packagesPath: File('.dart_tool/package_config.json').absolute.path,
metadata: {'name': '@scope/counter', 'version': '0.0.0'},
);
}
Run dart run tool/build.dart, then pack/install npm-package in the Node
consumer. Use a new output path, or follow the managed rebuild rules
for a package created by this builder on local macOS APFS.
For native, supply target: BuildTarget.native; the builder downloads and caches
the matching precompiled runtime. See native prerequisites and
explicit dual-target builds.
Use external selections for unmodified
Dart dependencies, or buildLibrary when raw artifacts are wanted.
See migration notes for the
former annotation, container and runtime assumptions.
Scalar conversions #
The same conversion rules apply to package imports on both targets. The example below uses the supported direct-Wasm path; ship its generated dependencies too.
import 'package:napi/napi.dart';
@NAPI()
String? echo(String? value) => value;
@NAPI(bigint: true)
int addWide(int left, int right) => left + right;
import { echo, addWide } from './module.wasm';
echo(null); // null
echo('🙂'); // '🙂'
addWide(1n, 2n); // 3n
Conversions never coerce input values. Required nullable parameters accept null, but not
undefined or omission. Optional parameters default for omission and undefined.
double preserves NaN, infinities and negative zero;
strings preserve UTF-16 code units, including lone surrogates. Default int
checks the JavaScript safe-integer range in both directions. The bigint option
maps all Dart int inputs/results of that function to JavaScript bigint with a
signed 64-bit input range; Dart BigInt always uses arbitrary-precision JavaScript
bigint. See the full contract, including error behavior.
Verify #
The current tested toolchain is Dart 3.13.5 stable, Node 24.19.0 and 26.11.1, and TypeScript 7.0.2. The SDK adapter deliberately rejects other Dart versions. Dart's Wasm integration remains experimental. Generated conformance consumers use both Node versions; native execution is qualified only for the experimental macOS arm64 ABI 15 runtime bundle supplied by the author. See test commands and native verification. Other native targets, Bun and browser compatibility remain unverified.
dart pub get
npm install --prefix .dart_tool/js --no-save --no-package-lock --ignore-scripts typescript@7.0.2
dart format --output=none --set-exit-if-changed lib test tool
dart analyze --fatal-infos
dart test
dart test -P callback-wasm
dart run tool/verify.dart
dart run tool/check_package.dart
tool/verify.dart builds fresh artifacts and executes the real Node ESM consumers,
including an npm-packed fixture installed offline into a separate consumer.
Set NAPI_NODE_EXECUTABLES to a JSON array of absolute Node executable paths
(e.g. '["/path/to/node24","/path/to/node26"]') to compile and install once,
then run each runtime and TypeScript consumer against the same artifacts on
every selected Node. The same setting works for tool/native_verify.dart; an
unset value uses node from PATH. Any selected runtime failure stops the run.
CI checks the Wasm artifacts on Node 24.19.0 and 26.11.1. Independent Dart unit
probes still use their configured/default Node; this setting controls the
conformance tools. Callback consumers use the callback-wasm and
callback-native package:test presets: each builds source/external once, then
runs every scenario on each selected Node. Their eight TypeScript configurations
run only on NAPI_TEST_NODE or PATH node, independently of the runtime matrix.
See Running tests for the explicit native bundle command and
single-scenario selection.
It checks TypeScript in NodeNext and Bundler modes. NAPI_TSC can select an
already installed TypeScript executable. Package validation permits only the
expected warning for the exact analyzer pin; every other warning or error fails.
To inspect just the generated scalar fixture:
dart run tool/scalar_fixture.dart .dart_tool/scalar-example
node .dart_tool/scalar-example/test.mjs
Relative .wasm imports use a sibling module.d.wasm.ts and TypeScript's
allowArbitraryExtensions option. A single-target Wasm package's root and
./module.wasm subpath both resolve directly to the Wasm file.
See the Wasm backend design for export representation, automatic initialization, SDK coupling and fresh-output publication. See npm packaging for managed rebuilds on macOS APFS and preservation of author files and metadata.
License #
MIT. See LICENSE. Generated Dart runtime helpers originate in the Dart
SDK. Generated artifacts include its license as DART_LICENSE; distributors
must retain that notice.