Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
05943f9
docs: add JS DSL high-level design spec
nazarhussain Mar 24, 2026
0057379
docs: address spec review issues — complete Value type, clarify Promi…
nazarhussain Mar 24, 2026
3649fff
docs: add migration strategy for backwards-compatible namespace restr…
nazarhussain Mar 24, 2026
195e582
docs: add JS DSL implementation plan and fix review issues
nazarhussain Mar 24, 2026
70a6923
docs: fix throwError param type and String.toOwnedSlice memory safety
nazarhussain Mar 24, 2026
4875ed7
feat(js): add namespace with primitive type wrappers
nazarhussain Mar 24, 2026
b7baff0
feat(js): add complex type wrappers
nazarhussain Mar 24, 2026
71cbd5d
fix(js): address spec review issues for complex types
nazarhussain Mar 24, 2026
b8b7b2c
feat(js): add wrapFunction comptime callback generator
nazarhussain Mar 24, 2026
fa0f207
feat(js): add wrapClass comptime class generator
nazarhussain Mar 24, 2026
39f71b7
feat(js): add exportModule for auto-registering pub decls
nazarhussain Mar 24, 2026
c492482
feat(js): export comptime machinery from entry point
nazarhussain Mar 24, 2026
194647b
fix(js): dereference self pointer for by-value class methods
nazarhussain Mar 24, 2026
ede21bd
feat(js): add DSL example module with integration tests
nazarhussain Mar 24, 2026
4cd30ba
docs: add README restructure and examples design spec
nazarhussain Mar 24, 2026
c848ffa
docs: add README restructure and examples implementation plan
nazarhussain Mar 24, 2026
e740853
feat(js): expand DSL example module with all types and patterns
nazarhussain Mar 24, 2026
0a33993
test(js): expand DSL integration tests covering all types and patterns
nazarhussain Mar 24, 2026
36134f1
docs: restructure README with DSL-first documentation
nazarhussain Mar 24, 2026
b07159e
build: add zapi module alias and js_dsl example to zbuild.zon
nazarhussain Mar 24, 2026
fd42d68
chore: remove superpowers docs from git and add to gitignore
nazarhussain Mar 24, 2026
11cb84e
fix(js): address code review issues
nazarhussain Mar 25, 2026
8a17eeb
fix(js): address PR review feedback
nazarhussain Mar 25, 2026
e944e15
chore: update .gitignore
nazarhussain Mar 26, 2026
dadea7f
Merge branch 'main' into nh/zapi-dsl
nazarhussain Mar 26, 2026
ec639dc
fix(js): validate typed array subtypes, array element types,
nazarhussain Mar 26, 2026
7303445
chore: add link_libc
nazarhussain Mar 31, 2026
1740ad1
feat(js): add exportModuleWithOptions with lifecycle hooks and env re…
nazarhussain Mar 31, 2026
07fdcdb
feat(js): export exportModuleWithOptions from js.zig
nazarhussain Mar 31, 2026
787d922
feat(js): add lifecycle example and worker thread tests
nazarhussain Mar 31, 2026
a178d91
refactor(js): unify exportModule and
nazarhussain Mar 31, 2026
5553f0b
feat(js): add recursive namespace support to registerDecls
nazarhussain Apr 1, 2026
e49e53b
feat(js): add namespace example sub-modules and integration tests
nazarhussain Apr 1, 2026
ff36ceb
fix(js): use inline for in hasDslDecls for comptime field access
nazarhussain Apr 1, 2026
9ef6589
feat: add static method support
nazarhussain Apr 1, 2026
8274d47
feat: support getter and setters
nazarhussain Apr 1, 2026
4cd74d4
feat: add instance method factory support
nazarhussain Apr 1, 2026
0aa4129
feat: add support for custom register at exportModule
nazarhussain Apr 1, 2026
e40f8d1
fix: update wraper_class to improve comptime
nazarhussain Apr 2, 2026
64edd47
fix: update the zbuild to link libc
nazarhussain Apr 2, 2026
cbc1e15
fix: handle explicit undefined for optional DSL arguments
nazarhussain Apr 2, 2026
75b982f
fix: finalize placeholder instances in class factory wrappers
nazarhussain Apr 2, 2026
b4ec791
fix(js): handle u64 values above i64 max in Number.from
nazarhussain Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ test/spec/static_tests.zig
.yarn
node_modules/
lib/
docs/superpowers/
.claude/settings.local.json
242 changes: 208 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

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

## Overview

zapi provides two main components:

1. **Zig Library** (`src/`) - Idiomatic Zig bindings for the Node.js N-API, making it easy to write native addons in Zig
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
Expand All @@ -28,30 +26,205 @@ Add the Zig dependency to your `build.zig.zon`:

---

## Zig Library
## Zig Library — Quick Start

### 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.

```zig
const napi = @import("napi");
const js = @import("zapi").js;

comptime {
napi.module.register(initModule);
pub fn add(a: js.Number, b: js.Number) js.Number {
return js.Number.from(a.assertI32() + b.assertI32());
}

fn initModule(env: napi.Env, module: napi.Value) !void {
// Export a string
try module.setNamedProperty("greeting", try env.createStringUtf8("Hello from Zig!"));

// Export a function
try module.setNamedProperty("add", try env.createFunction("add", 2, napi.createCallback(2, add, .{}), null));
pub const Counter = struct {
pub const js_class = true;
count: i32,

pub fn init(start: js.Number) Counter {
return .{ .count = start.assertI32() };
}

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

pub fn getCount(self: Counter) js.Number {
return js.Number.from(self.count);
}
};

comptime { js.exportModule(@This()); }
```

**JavaScript usage:**

```js
const mod = require('./my_module.node');
mod.add(1, 2); // 3
const c = new mod.Counter(0);
c.increment();
c.getCount(); // 1
```

`pub` functions are auto-exported, and structs with `js_class = true` become JS classes. One line — `comptime { js.exportModule(@This()); }` — registers everything.

---

## JS Types Reference

| Type | JS Equivalent | Key Methods |
|------|--------------|-------------|
| `Number` | `number` | `toI32()`, `toF64()`, `assertI32()`, `from(anytype)` |
| `String` | `string` | `toSlice(buf)`, `toOwnedSlice(alloc)`, `len()`, `from([]const u8)` |
| `Boolean` | `boolean` | `toBool()`, `assertBool()`, `from(bool)` |
| `BigInt` | `bigint` | `toI64()`, `toU64()`, `toI128()`, `from(anytype)` |
| `Date` | `Date` | `toTimestamp()`, `from(f64)` |
| `Array` | `Array` | `get(i)`, `getNumber(i)`, `length()`, `set(i, val)` |
| `Object(T)` | `object` | `get()`, `set(value)` — `T` fields must be DSL types |
| `Function` | `Function` | `call(args)` |
| `Value` | `any` | `isNumber()`, `asNumber()`, type checking/narrowing |
| `Uint8Array` etc. | `TypedArray` | `toSlice()`, `from(slice)` |
| `Promise(T)` | `Promise` | `resolve(value)`, `reject(err)` |

---

## Functions

Three patterns for exporting functions:

### Basic — direct mapping

```zig
pub fn add(a: Number, b: Number) Number {
return Number.from(a.assertI32() + b.assertI32());
}
```

fn add(a: i32, b: i32) i32 {
return a + b;
### Error handling — `!T` becomes a thrown JS exception

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

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

### Nullable returns — `?T` becomes `undefined`

```zig
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_class = true` are exported as JavaScript classes:

```zig
pub const Timer = struct {
pub const js_class = true;
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;
}
};
```

**Rules:**

- `pub const js_class = true` — marker that identifies the struct as a JS class
- `pub fn init(...)` — constructor (must return `T` or `!T`)
- `pub fn method(self: *T, ...)` — mutable instance method
- `pub fn method(self: T, ...)` — immutable instance method
- `pub fn method(...)` — static method (no self)
- `pub fn deinit(self: *T)` — optional GC destructor

---

## Working with Types

### Typed Objects

```zig
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

```zig
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);
}
```

### Promises

```zig
pub fn asyncOp(val: Number) !Promise(Number) {
var promise = try js.createPromise(Number);
try promise.resolve(val); // or dispatch async work
return promise;
}
```

### Callbacks

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

---

## Mixing DSL and N-API

```zig
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 };
}
```

`js.env()` gives access to the full N-API when you need it. `js.allocator()` provides the C allocator.

---

## 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

| Type | Description |
Expand Down Expand Up @@ -86,6 +259,8 @@ fn add_manual(env: napi.Env, info: napi.CallbackInfo(2)) !napi.Value {
Let zapi handle argument/return conversion:

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

// Arguments and return value are automatically converted
fn add(a: i32, b: i32) i32 {
return a + b;
Expand Down Expand Up @@ -118,9 +293,11 @@ napi.createCallback(2, myFunc, .{
### Creating Classes

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

const Timer = struct {
start: i64,

pub fn read(self: *Timer) i64 {
return std.time.milliTimestamp() - self.start;
}
Expand All @@ -142,6 +319,8 @@ try env.defineClass(
Run CPU-intensive work off the main thread:

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

const Work = struct {
a: i32,
b: i32,
Expand Down Expand Up @@ -170,6 +349,8 @@ try work.queue();
Call JavaScript from any thread:

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

const tsfn = try env.createThreadsafeFunction(
jsCallback, // JS function to call
context, // User context
Expand All @@ -190,10 +371,12 @@ try tsfn.call(&data, .blocking);
All N-API calls return `NapiError` on failure:

```zig
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");
Expand Down Expand Up @@ -386,24 +569,15 @@ Resolution order:

---

## Example

See the [example/](example/) directory for a comprehensive example including:
- String properties
- Functions with manual and automatic argument handling
- Classes with methods
- Async work with promises
- Thread-safe functions

```bash
# Build the example
zig build
## Examples

# Test it
node example/test.js
```
See the [examples/](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 lifecycle management
- Callbacks and mixed DSL/N-API usage
- Low-level N-API with manual registration

## License

MIT

39 changes: 39 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ pub fn build(b: *std.Build) void {
tls_install_lib_example_type_tag.dependOn(&install_lib_example_type_tag.step);
b.getInstallStep().dependOn(&install_lib_example_type_tag.step);

const module_example_js_dsl = b.createModule(.{
.root_source_file = b.path("examples/js_dsl/mod.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
b.modules.put(b.dupe("example_js_dsl"), module_example_js_dsl) catch @panic("OOM");

const lib_example_js_dsl = b.addLibrary(.{
.name = "example_js_dsl",
.root_module = module_example_js_dsl,
.linkage = .dynamic,
});

lib_example_js_dsl.linker_allow_shlib_undefined = true;
const install_lib_example_js_dsl = b.addInstallArtifact(lib_example_js_dsl, .{
.dest_sub_path = "example_js_dsl.node",
});

const tls_install_lib_example_js_dsl = b.step("build-lib:example_js_dsl", "Install the example_js_dsl library");
tls_install_lib_example_js_dsl.dependOn(&install_lib_example_js_dsl.step);
b.getInstallStep().dependOn(&install_lib_example_js_dsl.step);

const tls_run_test = b.step("test", "Run all tests");

const test_zapi = b.addTest(.{
Expand Down Expand Up @@ -109,9 +132,25 @@ pub fn build(b: *std.Build) void {
tls_run_test_example_type_tag.dependOn(&run_test_example_type_tag.step);
tls_run_test.dependOn(&run_test_example_type_tag.step);

const test_example_js_dsl = b.addTest(.{
.name = "example_js_dsl",
.root_module = module_example_js_dsl,
.filters = b.option([][]const u8, "example_js_dsl.filters", "example_js_dsl test filters") orelse &[_][]const u8{},
});
const install_test_example_js_dsl = b.addInstallArtifact(test_example_js_dsl, .{});
const tls_install_test_example_js_dsl = b.step("build-test:example_js_dsl", "Install the example_js_dsl test");
tls_install_test_example_js_dsl.dependOn(&install_test_example_js_dsl.step);

const run_test_example_js_dsl = b.addRunArtifact(test_example_js_dsl);
const tls_run_test_example_js_dsl = b.step("test:example_js_dsl", "Run the example_js_dsl test");
tls_run_test_example_js_dsl.dependOn(&run_test_example_js_dsl.step);
tls_run_test.dependOn(&run_test_example_js_dsl.step);

module_zapi.addImport("build_options", options_module_build_options);

module_example_hello_world.addImport("zapi", module_zapi);

module_example_type_tag.addImport("zapi", module_zapi);

module_example_js_dsl.addImport("zapi", module_zapi);
}
Loading
Loading