napi 0.3.0 copy "napi: ^0.3.0" to clipboard
napi: ^0.3.0 copied to clipboard

Export Dart functions as native WebAssembly ES modules with JavaScript interop and TypeScript declarations.

napi #

Write Dart functions. Import them directly from WebAssembly in JavaScript and TypeScript.

import { add } from './dist/module.wasm';

add(20, 22); // 42

The imported functions are real Wasm exports. No initialization call or JavaScript entrypoint is required.

Quick start #

Use Dart 3.13.5 or newer and Node 22.19+ in the 22.x line, or 24.5+:

dart pub add napi

Write lib/math.dart:

import 'dart:typed_data';
import 'package:napi/napi.dart';

@napi
double add(double a, double b) => a + b;

@napi
String greet(String name) => 'Hello, $name';

@napi
int checksum(Uint8List bytes) {
  var sum = 0;
  for (final byte in bytes) {
    sum = (sum + byte) & 0xff;
  }
  return sum;
}

Build:

dart run napi:build lib/math.dart --name @example/math --out dist --version 0.1.0

Then import the generated Wasm file from an ES module:

import { add, greet, checksum } from './dist/module.wasm';

console.log(add(20, 22));                     // 42
console.log(greet('Dart'));                  // Hello, Dart
console.log(checksum(new Uint8Array([1, 2]))); // 3

Or install the generated directory as an npm package:

npm install /absolute/path/to/dist
import { add } from '@example/math';
// Also supported: import { add } from '@example/math/module.wasm';

add(20, 22);

Both package entries point to module.wasm. The generated package has no npm runtime dependencies. Keep module.imports.mjs beside the Wasm file: the Wasm module imports these compiler and value-conversion helpers automatically.

Async functions #

Return a Dart Future<T> to export a JavaScript Promise<T>. Both async functions and ordinary functions returning a Future are supported:

@napi
Future<int> addLater(int a, int b) async {
  await Future<void>.delayed(const Duration(milliseconds: 1));
  return a + b;
}
import { addLater } from './dist/module.wasm';

const result: number = await addLater(20, 22); // 42
// Generated declaration: addLater(a: number, b: number): Promise<number>

Every call to a Future export returns a genuine Promise. Invalid arguments, synchronous throws, asynchronous failures, and invalid results reject it. Future<void> resolves to undefined; Future<T?> can resolve to null. Errors retain the categories below; Dart failures also expose a dartStack string. Original JavaScript exception objects and non-null primitive reasons retain their identity. The Dart SDK wraps JavaScript throw null, throw undefined, and Promise rejection with either value as Dart exceptions; napi reports these as readable JavaScript Error objects. Error or stack formatters that throw use a fallback message so rejection can still finish. Failures while reading a JavaScript error's stack cannot replace that error.

Byte inputs are copied during the call, before its first await; byte results are copied when the Future completes. Mutating or detaching the caller's input after the call cannot change the Dart snapshot. Futures use the Dart SDK's microtasks and timers; they do not imply execution on another thread or support for dart:io or isolates.

Lists and maps #

Export flat collections of bool, int, double, or String:

@napi
List<int> doubleAll(List<int> values) => [for (final value in values) value * 2];

@napi
Map<String, String> labels(Map<String, String> values) => {...values};
import { doubleAll, labels } from './dist/module.wasm';

const numbers: number[] = doubleAll([1, 2]); // [2, 4]
const names: Record<string, string> = labels({ greeting: 'hello' });

Containers and elements can be nullable: List<int?>? becomes Array<number | null> | null, and Map<String,String?>? becomes Record<string,string | null> | null. Future completion values follow the same rules, such as Future<List<int>> becoming Promise<number[]>.

Inputs are fully validated and copied before business code runs, including before returning a Promise. Outputs are new containers at return or Future completion. Empty containers are independent too. Values use the scalar rules below; undefined is rejected, including in nullable elements. Failures identify the parameter or result, with the index or key once available.

Lists accept dense Arrays, including cross-realm Arrays. Every index must be an own data property: holes, inherited indices, and index accessors are rejected without invoking getters. Extra named and symbol properties are ignored. Typed arrays continue to use Uint8List; they are not accepted as Lists.

Maps accept ordinary objects, including cross-realm objects, and null-prototype objects. Only own enumerable string data properties are read; accessors are rejected without invoking getters. Inherited, non-enumerable, and symbol properties are ignored. Class instances, custom prototypes, Arrays, Date, and JavaScript Map/Set are rejected. Outputs have a null prototype and retain keys such as __proto__, constructor, and then; use Object.hasOwn rather than calling methods on the returned object. Key order is not an API guarantee. Proxy reflection traps still execute as ordinary JavaScript operations; thrown exceptions follow the existing error policy.

Nested collections, byte-array elements, non-String or nullable Map keys, raw List/Map types, and aliases in exported signatures are rejected before compilation. Arbitrary objects and class wrappers are outside this release.

TypeScript #

Package imports resolve index.d.ts through exports.types with either NodeNext or Bundler module resolution.

For a relative import such as ./dist/module.wasm, napi also generates module.d.wasm.ts. Enable TypeScript's allowArbitraryExtensions:

{
  "compilerOptions": {
    "strict": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowArbitraryExtensions": true
  }
}

The declarations contain the actual exported signatures; no wildcard .wasm declaration is needed.

Supported exports #

Annotate public top-level functions in the entry library. Use explicit return types and required positional parameters. Extra arguments are ignored, as with ordinary native Wasm functions; missing arguments arrive as undefined and fail type checks.

Dart JavaScript / TypeScript Boundary behavior
void void Return type only; returns undefined
bool boolean No coercion
int number Input and output must be safe integers
double number Preserves NaN, Infinity, and negative zero
String string Preserves UTF-16 code units
Uint8List Uint8Array Copies in and out; Node Buffer and cross-realm arrays accepted
List<T> T[] Flat scalar elements; independent input/output Arrays
Map<String,T> Record<string,T> Flat scalar values; output has a null prototype
T? T | null Accepts null; rejects undefined
Future<T> Promise<T> Return type only; completion uses the same value rules

Invalid arguments throw TypeError; unsafe integers throw RangeError. Dart ArgumentError and TypeError become JavaScript TypeError, Dart RangeError becomes JavaScript RangeError, and other Dart exceptions become JavaScript Error with a readable message.

The export name then is reserved because dynamic ESM imports treat it as a promise callback. Dart compiler helper names such as $invokeMain and $wasmI16ArrayGet are also reserved; other $ names are allowed. Classes, generics, type aliases in exported signatures, optional/named parameters, generators, streams, and callbacks are not supported. Future parameters, nullable Futures, FutureOr, nested Futures, and async void exports are also rejected. Ordinary Dart helpers and Wasm-compatible dependencies can be used inside exported functions.

Status and platforms #

0.3.0 is experimental and targets Node's native Wasm ESM integration. Node documents instance-phase Wasm imports as experimental. Synchronous CommonJS require is not supported.

The backend uses Dart's experimental Wasm interop and compiler-generated JavaScript helpers. It rewrites the Wasm import section so Node resolves the helpers, string constants, and built-in string operations through ESM; business logic remains Dart Wasm. It does not implement the Node-API C ABI or produce .node addons.

Tested compiler: Dart 3.13.5. Other SDK versions allowed by the package constraint may change the experimental helper layout; unsupported layouts fail the build with a diagnostic. The generated Node engine requirement is ^22.19.0 || >=24.5.0.

Native browser instance imports and bundler compatibility are not supported in this release. A browser's support for WasmGC or source-phase imports alone does not establish support for import { add } from './module.wasm'. The roadmap tracks platform validation.

The build checks signatures before compiling, supports Dart workspaces, and refuses to overwrite unrelated output directories. Rebuilds stage a complete replacement on the destination filesystem and restore the old package if installation fails. Run dart pub get in the source package or workspace first. Use dart run napi:build --help for options.

Development #

dart pub get
dart format bin lib test example benchmark
dart analyze --fatal-infos
dart test
dart pub publish --dry-run

Integration tests build a real package and verify native functions through package, subpath, and relative Wasm imports. Set NAPI_TSC to TypeScript's bin/tsc to include the TypeScript consumer checks; CI supplies it. test/js/browser.html probes native browser loading without a fallback loader.

See the requirements, 0.3.0 milestone, and performance measurements.

MIT licensed. Generated host helpers include the Dart SDK's BSD license notice.

2
likes
0
points
691
downloads

Publisher

unverified uploader

Weekly Downloads

Export Dart functions as native WebAssembly ES modules with JavaScript interop and TypeScript declarations.

Repository (GitHub)
View/report issues

Topics

#wasm #node #interop

License

unknown (license)

Dependencies

analyzer, path, yaml

More

Packages that depend on napi