@chainsafe/zapi

中文文档
4.3.0发布于 2026/10/31MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1.1万

A tool for managing and publishing Zig NAPI packages.

原始使用文档
文档目录39 节
  1. Installation
  2. Zig addon project setup
  3. Zig Library — Quick Start
  4. JS Types Reference
  5. Functions
  6. Basic — direct mapping
  7. Error handling — !T becomes a thrown JS exception
  8. Nullable returns — ?T becomes undefined
  9. Classes
  10. Optional Parameters
  11. Getters and Setters
  12. Working with Types
  13. Typed Objects
  14. TypedArrays
  15. Promises
  16. Async Tasks
  17. Callbacks
  18. Namespaces
  19. Constants and Enums
  20. Module Lifecycle
  21. Mixing DSL and N-API
  22. Advanced: Low-Level N-API
  23. Core Types
  24. Creating Functions
  25. Creating Classes
  26. Async Work (Thread Pool)
  27. Thread-Safe Functions
  28. Error Handling
  29. CLI Tool
  30. Configuration
  31. Supported Targets
  32. Global Options
  33. Commands
  34. Release Workflow
  35. Error Handling
  36. Runtime Loading
  37. requireNapiLibrary(packageDir)
  38. Examples
  39. License

zapi

A Zig N-API wrapper library and CLI for building and publishing cross-platform Node.js native addons.

zapi provides two main components:

  1. Zig Library (src/) - Write Node.js native addons in Zig with a high-level DSL that mirrors JavaScript's type system
  2. CLI Tool (ts/) - Build tooling for cross-compiling and publishing multi-platform npm packages

Installation

npm install -D @chainsafe/zapi

Zig addon project setup

examples/ is a complete Zig consumer project with its own build.zig.zon and build.zig. The zapi package build does not configure the examples.

Most projects produce one addon. A second logical addon exists only when the package produces another final .node library with a separate root module. The examples project produces six addons; example_js_dsl.node and example_addon_isolation.node both use DSL classes and therefore each receive their own identity. A project does not need to produce multiple addons for the identity to matter: one Node process can load addons from unrelated packages, and each addon must reject class objects created by the others.

The consumer's build.zig configures the package, then attaches the generated identity to every final compile step whose module exports or exchanges a js.class:

const std = @import("std");
const zapi_build = @import("zapi");
const zbuild = @import("zbuild");

pub fn build(b: *std.Build) !void {
    @setEvalBranchQuota(200_000);
    const manifest = @import("build.zig.zon");
    const result = try zbuild.configureBuild(b, manifest, .{});

    zapi_build.addAddonIdentity(
        b,
        result.library("example_js_dsl").?,
        manifest,
    );
    zapi_build.addAddonIdentity(
        b,
        result.library("example_addon_isolation").?,
        manifest,
    );
}

The corresponding root modules pass the generated import to js.exportModule:

comptime {
    js.exportModule(@This(), .{
        .identity = @import("zapi_addon_identity"),
    });
}

See examples/js_dsl/mod.zig for a normal class addon and examples/addon_isolation/mod.zig for the same-named class in a second addon used to test cross-addon isolation. A function-only module does not need an identity; the complete project keeps examples/function_only_dsl/mod.zig on js.exportModule(@This(), .{}).

addAddonIdentity creates the zapi_addon_identity import from the consumer package name, version, fingerprint, and final compile-step name. Do not create that import as a source file. Distinct addons in one package need unique compile-step names and separate root modules. The identity does not use dest_sub_path or the path from which Node loads the file, so copies and filesystem aliases of the same compiled .node remain compatible.

The examples declare zapi and zbuild as URL/hash dependencies. Build them against this checkout with:

cd examples
zig build --fork=..

For another consumer project, use zig build normally. During local zapi development, keep the URL/hash dependency and override it with zig build --fork=/path/to/zapi; Zig 0.16 .path dependencies do not expose the dependency's build.zig as an import.


Zig Library — Quick Start

The DSL is the default approach for writing native addons. Import js from zapi and write normal Zig functions — zapi handles all the N-API marshalling automatically.

const js = @import("zapi").js;

pub fn add(a: js.Number, b: js.Number) !js.Number {
    return js.Number.from(try a.toI32() + try b.toI32());
}

pub const Counter = struct {
    pub const js_meta = js.class(.{
        .properties = .{
            .count = js.prop(.{ .get = true, .set = false }),
        },
    });

    _count: i32,

    pub fn init(start: js.Number) !Counter {
        return .{ ._count = try start.toI32() };
    }

    pub fn increment(self: *Counter) void {
        self._count += 1;
    }

    // Getter: obj.count (not obj.count())
    pub fn count(self: Counter) js.Number {
        return js.Number.from(self._count);
    }
};

comptime {
    js.exportModule(@This(), .{
        .identity = @import("zapi_addon_identity"),
    });
}

JavaScript usage:

const mod = require('./my_module.node');
mod.add(1, 2); // 3
const c = new mod.Counter(0);
c.increment();
c.count; // 1 (getter, not a method call)

pub functions are auto-exported, and structs with js_meta = js.class(...) become JS classes. Class type tags are derived at compile time from the Zig package name, version, fingerprint, addon artifact name, and class type name. This keeps two loaded copies of one addon compatible while isolating different addons and package versions. Consequently, addons built from v1.0.0 and v1.0.1 cannot exchange DSL class objects in the same process, even if their native class definitions are otherwise compatible. Modules that export only functions and never accept or return DSL classes may continue to use js.exportModule(@This(), .{}).

FNV-1a-128(package@version#fingerprint::addon::ZigType)

Low-level wrapper and conversion APIs take the same identity type explicitly. Code paths that cannot accept or return DSL classes pass js.NoAddonIdentity.


JS Types Reference

TypeJS EquivalentKey Methods
NumbernumbertoI32(), toF64(), assertI32(), from(anytype)
StringstringtoSlice(buf), toOwnedSlice(alloc), len(), from([]const u8)
BooleanbooleantoBool(), assertBool(), from(bool)
BigIntbiginttoI64(), toU64(), toI128(), from(anytype)
DateDatetoTimestamp(), from(f64)
ArrayArrayget(i), getNumber(i), length(), set(i, val)
Object(T)objectget(), set(value) — T fields must be DSL types
FunctionFunctioncall(args)
ValueanyisNumber(), asNumber(), type checking/narrowing
Uint8Array etc.TypedArraytoSlice(), toArray(len), from(slice)
OwnedUint8Array etc.TypedArrayfromOwnedSlice(allocator, data), fromSlice(allocator, data)
Promise(T)Promiseresolve(value), reject(err)

Functions

Three patterns for exporting functions:

Basic — direct mapping

pub fn add(a: Number, b: Number) !Number {
    return Number.from(try a.toI32() + try b.toI32());
}

Error handling — !T becomes a thrown JS exception

pub fn safeDivide(a: Number, b: Number) !Number {
    const divisor = try b.toI32();
    if (divisor == 0) return error.DivisionByZero;
    return Number.from(@divTrunc(try a.toI32(), divisor));
}

JS: try { safeDivide(10, 0) } catch (e) { /* "DivisionByZero" */ }

Nullable returns — ?T becomes undefined

pub fn findValue(arr: Array, target: Number) ?Number {
    const len = arr.length() catch return null;
    // ... search, return null if not found
}

Classes

Structs with js_meta = js.class(...) are exported as JavaScript classes:

pub const Timer = struct {
    pub const js_meta = js.class(.{});
    start: i64,

    pub fn init() Timer {
        return .{ .start = std.time.milliTimestamp() };
    }

    pub fn elapsed(self: Timer) js.Number {
        return js.Number.from(std.time.milliTimestamp() - self.start);
    }

    pub fn reset(self: *Timer) void {
        self.start = std.time.milliTimestamp();
    }

    pub fn deinit(self: *Timer) void {
        _ = self;
    }
};

Method classification:

SignatureJS Behavior
pub fn init(...)Constructor (new Class(...)) — must return T or !T
pub fn method(self: T, ...)Immutable instance method
pub fn method(self: *T, ...)Mutable instance method
pub fn method(self: T, ...) !TInstance method returning a new JS instance
pub fn method(...) !TStatic method returning a new JS instance
pub fn method(...)Static method (no self, returns non-T)
pub fn deinit(self: *T)Optional GC destructor

Methods or functions that return the class type automatically materialize a fresh JS instance. There is no separate author-facing "factory" marker:

pub const PublicKey = struct {
    pub const js_meta = js.class(.{});
    pk: bls.PublicKey,

    pub fn init() PublicKey {
        return .{ .pk = undefined };
    }

    // Static factory: PublicKey.fromBytes(bytes)
    pub fn fromBytes(bytes: js.Uint8Array) !PublicKey {
        const slice = try bytes.toSlice();
        return .{ .pk = try bls.PublicKey.deserialize(slice) };
    }
};

JS: const pk = PublicKey.fromBytes(bytes);

Same-class instance methods also work:

pub fn clone(self: MyState) !MyState {
    const cloned = try self.data.clone();
    return .{ .data = cloned };
}

JS: const newState = state.clone(); — returns a new instance, original unchanged.

Optional Parameters

Parameters with optional DSL types (?js.Number, ?js.Boolean, etc.) become optional JS arguments:

pub fn fromBytes(bytes: js.Uint8Array, validate: ?js.Boolean) !PublicKey {
    const do_validate = if (validate) |v| try v.toBool() else false;
    // ...
}

JS: PublicKey.fromBytes(bytes) or PublicKey.fromBytes(bytes, true)

Getters and Setters

Declare properties inside js_meta with js.prop to register property accessors:

pub const Config = struct {
    pub const js_meta = js.class(.{
        .properties = .{
            .volume = js.prop(.{ .get = true, .set = true }),
            .muted = js.prop(.{ .get = true, .set = true }),
            .label = js.prop(.{ .get = true, .set = false }),
        },
    });

    _volume: i32,
    _muted: bool,
    _label: []const u8,

    pub fn init() Config {
        return .{ ._volume = 50, ._muted = false, ._label = "default" };
    }

    // Read-write: obj.volume / obj.volume = 80
    pub fn volume(self: Config) js.Number {
        return js.Number.from(self._volume);
    }
    pub fn setVolume(self: *Config, value: js.Number) !void {
        const v = try value.toI32();
        if (v < 0 or v > 100) return error.VolumeOutOfRange;
        self._volume = v;
    }

    // Read-only: obj.label
    pub fn label(self: Config) js.String {
        return js.String.from(self._label);
    }
};

JS: cfg.volume = 80; cfg.label; // "default"

Rules:

  • pub const js_meta = js.class(.{}) marks a struct as a JS class
  • .properties = .{ .name = js.prop(.{ .get = true, .set = false }) } registers a readonly getter backed by pub fn name(...)
  • .properties = .{ .name = js.prop(.{ .get = true, .set = true }) } registers getter/setter methods using name and setName
  • .properties = .{ .name = js.prop(.{ .get = "customGetter", .set = false }) } registers a getter backed by a specifically named method
  • Accessor backing methods are not exported as callable JS methods

Working with Types

Typed Objects

const Config = struct { host: String, port: Number, verbose: Boolean };

pub fn connect(config: Object(Config)) !String {
    const c = try config.get();
    // access c.host, c.port, c.verbose
}

TypedArrays

pub fn sum(data: Uint8Array) !Number {
    const slice = try data.toSlice();
    var total: i32 = 0;
    for (slice) |byte| total += @intCast(byte);
    return Number.from(total);
}

Use an owned return type to transfer an allocator-owned slice to JavaScript without copying its elements:

pub fn serialize() !js.OwnedUint8Array {
    const allocator = js.allocator();
    const data = try allocator.alloc(u8, 32);
    // Fill data...
    return js.OwnedUint8Array.fromOwnedSlice(allocator, data);
}

Returning the value transfers its allocation to JavaScript without copying and leaves the Zig owner empty. Failures before N-API accepts the external memory leave ownership in Zig so it can be released normally. The allocator must remain valid until the ArrayBuffer finalizer runs. If external ArrayBuffers are unsupported, the original error is returned; no copy fallback is performed.

Promises

pub fn asyncOp(val: Number) !Promise(Number) {
    var promise = try js.createPromise(Number);
    try promise.resolve(val);  // must resolve or reject before returning
    return promise;
}

Promise(T) in this DSL path is synchronous-only: resolve or reject it before the exported function returns. For work that must run off the JS thread, use js.spawn below.

Async Tasks

js.spawn runs a task on the libuv worker pool and returns a JS Promise that settles when it finishes. A task is any struct with compute, resolve, and deinit:

const ScaleTask = struct {
    data: []u32,
    factor: u32,

    // Worker thread — must not touch napi or DSL values.
    pub fn compute(self: *ScaleTask) !void {
        for (self.data) |*value| value.* *= self.factor;
    }

    // JS thread — the DSL env context is established, so DSL types work here.
    pub fn resolve(self: *ScaleTask, _: napi.Env) !js.OwnedUint32Array {
        const owned: js.OwnedUint32Array = .fromOwnedSlice(js.allocator(), self.data);
        self.data = &.{};  // ownership handed to JS, no copy
        return owned;
    }

    // Safe to call after resolve transferred ownership.
    pub fn deinit(self: *ScaleTask) void {
        js.allocator().free(self.data);
    }
};

pub fn asyncScale(data: js.Uint32Array, factor: Number) !Value {
    const copy = try js.allocator().dupe(u32, try data.toSlice());
    errdefer js.allocator().free(copy);
    return js.spawn(ScaleTask, .{ .data = copy, .factor = @intCast(factor.assertI32()) }, "asyncScale");
}

resolve may return a DSL type (js.Number), an owned typed array (transferred without copying), napi.Value, or void.

If compute returns an error the promise rejects with Error(@errorName(err)). Add an optional reject(self: *Task, env: napi.Env, err: anyerror) !napi.Value to build the rejection value yourself — js.errorWithMessage(env, msg) builds a plain Error for the common case:

pub fn reject(_: *Task, env: napi.Env, err: anyerror) !napi.Value {
    return js.errorWithMessage(env, switch (err) {
        error.ComputeFailed => "worker could not finish the job",
        else => @errorName(err),
    });
}

Ownership: if spawn fails the task is not consumed, so the caller's errdefers must free it (as above). Once spawn succeeds the helper owns the task and calls deinit after the promise settles.

Callbacks

pub fn applyCallback(val: Number, cb: Function) !Value {
    return try cb.call(.{val});
}

Namespaces

Import Zig modules as pub const to create JS namespaces. The DSL recursively registers all DSL-compatible declarations:

// root.zig
pub const math = @import("math.zig");     // → exports.math.multiply(...)
pub const crypto = @import("crypto.zig"); // → exports.crypto.PublicKey, etc.

comptime {
    js.exportModule(@This(), .{
        .identity = @import("zapi_addon_identity"),
    });
}

Namespaces nest arbitrarily — a sub-module with more pub const imports creates deeper nesting.


Constants and Enums

Scalar and string pub const decls (int, float, bool, []const u8) export as enumerable value properties — at the module root, inside namespaces, and on classes (as statics on the constructor):

pub const SHUFFLE_ROUNDS_MAINNET: u32 = 90; // → exports.SHUFFLE_ROUNDS_MAINNET
pub const LIBRARY = "zapi";                 // → exports.LIBRARY

A pub const enum exports as a frozen plain object mapping each tag name (verbatim, no case conversion) to its integer value — also at the module root, inside namespaces, and on classes:

pub const ByteCount = enum(u8) { one = 1, two = 2 }; // → exports.ByteCount = {one: 1, two: 2}

pub var decls are mutable state, not constants, and are never exported. Other const shapes (struct values, arrays, etc.) are skipped; export those via the .register hook. Integer consts and enum tags must fit in an i64 (compile error otherwise).


Module Lifecycle

exportModule accepts optional lifecycle hooks with atomic env refcounting:

comptime {
    js.exportModule(@This(), .{
        .identity = @import("zapi_addon_identity"),
        .init = fn (refcount: u32) !void,    // called before registration (0 = first env)
        .cleanup = fn (refcount: u32) void,  // called on env exit (0 = last env)
    });
}

This enables safe shared-state initialization for worker thread scenarios.


Mixing DSL and N-API

pub fn advanced() !Value {
    const e = js.env();      // access low-level napi.Env
    const obj = try e.createObject();
    // use any napi.Env method...
    return .{ .val = obj };
}

Context accessors:

FunctionDescription
js.env()Current N-API environment (thread-local, set by DSL callbacks)
js.io()Shared std.Io handle retained for the addon while at least one N-API environment is active
js.allocator()C allocator for native allocations
js.scratch()Per-call arena for temporary allocations, freed when the outermost callback returns
js.thisArg()JS this value (available inside instance methods/getters/setters)

js.scratch() removes the free bookkeeping for short-lived buffers:

pub fn shout(name: String) !String {
    const s = try name.toOwnedSlice(js.scratch()); // no free needed
    return String.from(try std.ascii.allocUpperString(js.scratch(), s));
}

Nested callbacks (Zig calls JS, which calls back into Zig) share the arena, so outer memory stays valid until the outermost callback returns. Use js.allocator() for anything that outlives the call, such as class fields, finalizer-owned data, or work done in js.spawn's compute.


Advanced: Low-Level N-API

The DSL layer handles most use cases. Drop down to the N-API layer when you need full control over handle scopes, async work, thread-safe functions, or other advanced features.

Core Types

TypeDescription
EnvThe N-API environment, provides methods to create values, throw errors, manage scopes
ValueA JavaScript value handle with methods for type checking, property access, conversions
CallbackInfoProvides access to function arguments and this binding
HandleScopePrevents garbage collection of values within a scope
EscapableHandleScopeLike HandleScope but allows one value to escape
RefA persistent reference to a value that survives garbage collection
DeferredResolver/rejecter for promises
AsyncWorkRun work on a thread pool with completion callback on main thread
ThreadSafeFunctionCall JavaScript from any thread safely
AsyncContextContext for async resource tracking

Creating Functions

Manual Style

Full control using raw Env and Value:

fn add_manual(env: napi.Env, info: napi.CallbackInfo(2)) !napi.Value {
    const a = try info.arg(0).getValueInt32();
    const b = try info.arg(1).getValueInt32();
    return try env.createInt32(a + b);
}

Automatic Conversion with createCallback

Let zapi handle argument/return conversion:

const napi = @import("zapi").napi;

// Arguments and return value are automatically converted
fn add(a: i32, b: i32) i32 {
    return a + b;
}

// Register with automatic wrapping
try env.createFunction("add", 2, napi.createCallback(2, add, .{}), null);

Argument Hints

Control how arguments are converted:

napi.createCallback(2, myFunc, .{
    .args = .{ .env, .auto, .value, .data, .string, .buffer },
    .returns = .value,  // or .string, .buffer, .external_buffer, .auto
});
HintDescription
.autoAutomatic type conversion; byte-slice returns are copied into a JS Buffer
.envInject napi.Env
.valuePass raw napi.Value
.dataUser data pointer passed to createFunction
.stringConvert to/from []const u8
.bufferBorrow Buffer arguments as byte slices; copy byte-slice returns
.external_bufferTransfer an OwnedBuffer to JavaScript without copying

External buffers require explicit ownership transfer:

const allocator = std.heap.page_allocator;

fn makeExternalBuffer() !napi.OwnedBuffer {
    const data = try allocator.dupe(u8, "external data");
    return napi.OwnedBuffer.fromOwnedSlice(allocator, data);
}

const callback = napi.createCallback(0, makeExternalBuffer, .{
    .returns = .external_buffer,
});

OwnedBuffer.intoValue empties the source after ownership transfers, so a deferred deinit is safe. Failures before N-API accepts the external memory leave ownership in the source; later failures leave it empty because the finalizer may already own the allocation. Once N-API accepts the external buffer, the allocator must remain valid until its finalizer runs. If the environment disallows external buffers, intoValue copies the bytes and consumes the source only after that copy succeeds.

Creating Classes

const napi = @import("zapi").napi;

const Timer = struct {
    start: i64,

    pub fn read(self: *Timer) i64 {
        return std.time.milliTimestamp() - self.start;
    }
};

try env.defineClass(
    "Timer",
    0,
    timerConstructor,
    null,
    &[_]napi.c.napi_property_descriptor{
        .{ .utf8name = "read", .method = napi.wrapCallback(0, Timer.read) },
    },
);

Async Work (Thread Pool)

Run CPU-intensive work off the main thread:

const napi = @import("zapi").napi;

const Work = struct {
    a: i32,
    b: i32,
    result: i32,
    deferred: napi.Deferred,
};

fn execute(env: napi.Env, data: *Work) void {
    // Runs on thread pool - don't call JS here!
    data.result = data.a + data.b;
}

fn complete(env: napi.Env, status: napi.status.Status, data: *Work) void {
    // Back on main thread - resolve the promise
    const result = env.createInt32(data.result) catch return;
    data.deferred.resolve(result) catch return;
}

// Create async work
const work = try napi.AsyncWork(Work).create(env, null, name, execute, complete, &data);
try work.queue();

Thread-Safe Functions

Call JavaScript from any thread:

const napi = @import("zapi").napi;

const tsfn = try env.createThreadsafeFunction(
    jsCallback,        // JS function to call
    context,           // User context
    "name",
    0,                 // Max queue size (0 = unlimited)
    1,                 // Initial thread count
    null,              // Finalize data
    null,              // Finalize callback
    myCallJsCallback,  // Called on main thread
);

// From any thread:
try tsfn.call(&data, .blocking);

Error Handling

All N-API calls return NapiError on failure:

const napi = @import("zapi").napi;

fn myFunction(env: napi.Env) !void {
    // Errors propagate naturally
    const value = try env.createStringUtf8("hello");

    // Throw JavaScript errors
    try env.throwError("ERR_CODE", "Something went wrong");
    try env.throwTypeError("ERR_TYPE", "Expected a number");
}

CLI Tool

Configuration

Add a zapi field to your package.json:

{
  "name": "my-addon",
  "zapi": {
    "binaryName": "my-addon",
    "step": "my-lib",
    "targets": [
      "x86_64-unknown-linux-gnu",
      "x86_64-unknown-linux-musl",
      "aarch64-unknown-linux-gnu",
      "aarch64-unknown-linux-musl",
      "x86_64-apple-darwin",
      "aarch64-apple-darwin",
      "x86_64-pc-windows-msvc"
    ]
  }
}

Supported Targets

TargetPlatformArchABI
aarch64-apple-darwinmacOSarm64-
x86_64-apple-darwinmacOSx64-
aarch64-unknown-linux-gnuLinuxarm64glibc
aarch64-unknown-linux-muslLinuxarm64musl
x86_64-unknown-linux-gnuLinuxx64glibc
x86_64-unknown-linux-muslLinuxx64musl
x86_64-pc-windows-msvcWindowsx64msvc

Global Options

OptionDescription
--help, -hShow help message
--version, -vShow version number

Commands

zapi build

Build for a single target platform.

zapi build [options]
OptionDescriptionDefault
--stepZig build stepzapi.step from package.json
--targetTarget tripleCurrent platform
--optimizeDebug, ReleaseSafe, ReleaseFast, ReleaseSmall-
--zig-cwdWorking directory for zig build.

zapi build-artifacts

Build for all configured targets and collect artifacts.

zapi build-artifacts [options]
OptionDescriptionDefault
--stepZig build stepzapi.step from package.json
--optimizeOptimization level-
--zig-cwdWorking directory for zig build.
--artifacts-dirOutput directory for artifactsartifacts

Example output:

▶ Building my-addon for 6 target(s)...
[1/6] Building for x86_64-unknown-linux-gnu...
  → Moving artifact to artifacts/x86_64-unknown-linux-gnu
[2/6] Building for aarch64-apple-darwin...
  → Moving artifact to artifacts/aarch64-apple-darwin
...
✓ Built 6 artifact(s) to artifacts/

zapi prepublish

Prepare npm packages for publishing:

  • Creates npm/<target>/ directories for each target
  • Copies compiled .node binaries from artifacts into target packages
  • Generates package.json for each target package (with correct os, cpu, libc)
  • Updates native entries in the main package.json's optionalDependencies, preserving unrelated entries
zapi prepublish [options]
OptionDescriptionDefault
--artifacts-dirDirectory containing built artifactsartifacts
--npm-dirDirectory for npm packagesnpm
--platforms-onlyPrepare target packages without changing the main manifestfalse
--main-onlyUpdate only the main manifest’s optionalDependenciesfalse

--platforms-only and --main-only are mutually exclusive. Without either option, both package types are prepared. Preparation is local and makes no registry requests. Artifacts remain available for retries; a missing artifact fails preparation. Generated platform manifests inherit the wrapper's publishConfig.

Example output:

▶ Preparing [email protected] for publishing...
▶ Copying artifacts to npm packages...
  → x86_64-unknown-linux-gnu → npm/x86_64-unknown-linux-gnu/my-addon.node
▶ Generating target package.json files...
  → Created npm/x86_64-unknown-linux-gnu/package.json
✓ Prepared 6 target package(s) in npm/
▶ Updating package.json with optionalDependencies...
✓ Prepared main package

zapi publish

Publish all target-specific packages and the main package to npm.

zapi publish [options] [-- <npm-args>]
OptionDescriptionDefault
--npm-dirDirectory containing npm packagesnpm
--dry-runPreview what would be published without publishingfalse
--platforms-onlyLeave the main package to another publisher, such as Lernafalse
--skip-existingSkip exact versions already published; registry errors abort the publishfalse

Any arguments after -- are passed directly to npm publish (e.g., --access public, --tag beta).

For workspace releases managed by Lerna, build the artifacts, then run zapi prepublish --platforms-only and zapi publish --platforms-only --skip-existing -- --access public. These commands leave the main manifest unchanged. Set the main package's prepack script to zapi prepublish --main-only so its exact platform dependencies are generated during packaging, after Lerna checks the working tree. Lifecycle scripts must be enabled for that step. Lerna then publishes the main package alongside the workspace.

Publishing requires platform manifests matching the wrapper's current name and version, with their binaries present. zapi validates every platform before making registry requests. The default publishes platforms first and the wrapper last; --platforms-only assigns the wrapper to an external publisher. To publish just the wrapper, use npm publish.

With --skip-existing, version queries use the destination registry, including publishConfig and explicit npm CLI overrides. Existing versions are left untouched, including their dist-tags. This supports retrying a partial release; changing an existing version's dist-tags is a separate npm dist-tag operation. Dry runs perform local validation and, with --skip-existing, registry queries, but never publish.

Example dry-run:

zapi publish --dry-run
▶ [DRY RUN] Publishing 6 target package(s) + main package with concurrency 1...
[1/7] Would publish x86_64-unknown-linux-gnu
  → Directory: /path/to/npm/x86_64-unknown-linux-gnu
...
✓ [DRY RUN] Would publish 7 package(s); skipped 0

Release Workflow

GitHub releases are managed with release-please:

  1. Conventional commits merged to main update or create the release PR.
  2. Merging that PR tags a new GitHub release and bumps package.json.
  3. build.zig.zon is kept in sync from the same release-please version.
  4. The release workflow installs dependencies, runs pnpm build:js, and publishes the root package directly with npm publish via npm trusted publishing.
  5. No NPM_TOKEN secret is required for npm publish; GitHub Actions OIDC (id-token: write) is used together with --provenance.
  6. The published npm package is the JS distribution only (lib/ and ts/).

Error Handling

Set DEBUG=1 for full stack traces on errors.


Runtime Loading

requireNapiLibrary(packageDir)

Load the native addon, automatically selecting the correct binary for the current platform:

import { requireNapiLibrary } from "@chainsafe/zapi";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const addon = requireNapiLibrary(__dirname);

Resolution order:

  1. Local build: zig-out/lib/<binaryName>.node
  2. Published package: <pkg-name>-<target>

Examples

See the examples/ directory for comprehensive examples including:

  • All DSL types (Number, String, Boolean, BigInt, Date, Array, Object, TypedArrays, Promise)
  • Error handling and nullable returns
  • Classes with static factories, instance factories, and optional parameters
  • Computed getters and setters
  • Nested namespaces
  • Module lifecycle hooks (init/cleanup with worker thread refcounting)
  • Callbacks and mixed DSL/N-API usage
  • Low-level N-API with manual registration

License

MIT

版本与发布记录

@chainsafe/zapi 共发布 11 个版本,首次发布于 2026/02/04;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
4.3.0latest2026/10/319 天前
4.2.02026/10/3010 天前
4.1.02026/09/251 个月前
4.0.02026/08/192 个月前
3.1.02026/07/103 个月前
3.0.02026/07/024 个月前

关于 @chainsafe/zapi 的常见问题

@chainsafe/zapi 是什么?

A tool for managing and publishing Zig NAPI packages.

如何安装 @chainsafe/zapi?

在项目目录运行 npm install @chainsafe/zapi,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 zapi,可以用 npx zapi 直接运行。

@chainsafe/zapi 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chainsafe/zapi 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chainsafe/zapi 可以商用吗?

@chainsafe/zapi 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@chainsafe/zapi 的下载量有多少?

根据 npm 官方下载统计,@chainsafe/zapi 在 2026/10/10 – 2026/11/08 这 30 天内被下载 11,490 次,日均约 383 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chainsafe/zapi 还在维护吗?

最新版本 4.3.0 发布于 2026/10/31(9 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/02/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。