feat(js): add high-level DSL with JS-aligned types#11
Conversation
lodekeeper-z
left a comment
There was a problem hiding this comment.
zapi PR #11 — High-Level DSL Review
Great architectural PR. The comptime machinery in wrapFunction/wrapClass is well-designed, the setEnv/restoreEnv thread-local pattern correctly handles nested callbacks, the !T/?T/!?T return-type dispatch is clean, and the example coverage is excellent. This should meaningfully reduce NAPI boilerplate for lodestar-z bindings.
I have a few issues that need attention before this lands — one is a fundamental Zig limitation that affects the public API contract, and a few are safety/correctness gaps in the generated callbacks. Inline comments below.
Critical
1. exportModule exports ALL functions, not just pub fns
In Zig 0.12+, std.builtin.Type.Declaration no longer has an is_pub field — it was removed. So @typeInfo(Module).@"struct".decls iterates every declaration regardless of visibility. The README says "pub functions are auto-exported" but that's not true: any function in the file, public or private, will be picked up. If a user has private helpers with native Zig types (not DSL), wrapFunction will @compileError on them, making the whole module fail to compile. This is a breaking UX issue for non-trivial modules that have helper functions. Options:
- Require an explicit
pub const js_exports = .{add, Counter, ...}list (most explicit, no magic) - Document that all helper functions must be in a separate file
- Use a naming convention (e.g. skip
_-prefixed functions)
2. Missing argument count validation in wrapFunction and wrapMethod
After napi_get_cb_info, actual_argc reflects how many args JS actually passed. But the code always iterates 0..argc unconditionally, using zeroed raw_args[i] entries for slots beyond actual_argc. A JS caller that passes too few arguments silently gets a null napi_value wrapped in a DSL struct. The first meaningful operation on it (e.g. assertI32()) will panic and crash Node.js — rather than throwing a JS TypeError. Same issue exists in wrapMethod.
3. utf8name pointer in getPropertyDescriptors is not null-terminated
desc.utf8name = decl.name.ptr uses the raw pointer from a []const u8 slice. napi_define_class treats utf8name as a C string (const char*) and reads until \0. decl.name as a Zig slice is not guaranteed to be null-terminated in the type system — even if comptime string literals happen to have a trailing null in practice. Compare: exportModule correctly does decl.name ++ "" to get [:0]const u8. getPropertyDescriptors should do the same.
Important
4. TypedArray slice lifetime is too loose
toSlice() returns a []Element pointing directly into the ArrayBuffer's V8-managed backing buffer. This is safe only within the current callback frame, before any JS interop that could trigger GC. If the slice escapes (stored in a Zig struct, passed to an async work callback, etc.) it becomes a dangling pointer. NAPI pins the buffer during the callback, but not past it. The doc comment says "valid as long as the ArrayBuffer is alive" but that's not actionable — callers can't know when V8 will collect it. Consider: return a copied []Element (safer, clear ownership) and separately offer a withSlice(callback) API for zero-copy work within a bounded scope.
5. js_class presence checked but not its value
@hasDecl(InnerType, "js_class") is true for pub const js_class = false or pub const js_class = "oops". Should be @field(InnerType, "js_class") == true.
6. as*() narrowing methods don't actually validate the JS type
asNumber(), asString(), etc. are typed !T suggesting they can fail — but they blindly wrap the value without calling napi_typeof. Type errors are deferred to the first toI32()/toBool() call, which will return a NAPI error. At minimum, the docs should be updated. Ideally, these methods should actually check the type and return a descriptive error (e.g. error.TypeMismatch) if it doesn't match.
7. Async Promise model is incomplete for true async use
createPromise/resolve works for synchronous resolution but the intended async pattern (resolve from a work callback or another thread) requires napi_threadsafe_function + storing the napi_deferred across the callback boundary. Once napi_resolve_deferred is called the deferred is consumed. For actual async: the user needs to:
- Create the promise, extract the deferred
- Return the promise JS value
- Post work to another thread (via
napi.AsyncWork) - In the completion callback, call resolve/reject via a threadsafe function
The current Promise(T) struct doesn't help with steps 3–4. Consider adding a Promise.Resolver type that holds the deferred and can safely be passed to async work, or at least document the expected pattern.
Minor
Promise.reject()only acceptsjs.String. Can't reject with a JSErrorobject (the idiomatic JS rejection type). Consider also acceptingValueornapi.Value.toOwnedSliceallocates two buffers (one for the null-terminated read, one for the result). Could use a singlestr_len + 1allocation and slice off the null.Buffer.getSize()castsusizetoi32via@intCast— overflows for buffers > 2GiB (unlikely in practice, but worth a comment).- The
assert*()panic philosophy (crash Node.js on type mismatch) is a valid design choice, but the README should explicitly document it so users know to validate inputs before using assert variants.
|
@lodekeeper-z Please re-review this PR. |
lodekeeper-z
left a comment
There was a problem hiding this comment.
All five issues from the previous review have been addressed:
- export_module.zig: Now skips non-DSL functions (checks each param type), and validates
js_class == truewith type check (@TypeOf(...) == bool). ✅ - wrap_function.zig: Argument count validation is in place — throws
TypeErrorifactual_argc < argc. ✅ - wrap_class.zig:
utf8nameuses[:0]const u8(decl.name ++ "") so the pointer is null-terminated. ✅ - typed_arrays.zig:
toSlicenow has a clear doc comment warning about the V8-managed backing store lifetime.fromcopies data into a new ArrayBuffer. ✅ - value.zig: All
as*()narrowing methods now callnapi_typeof(orisArray/isDate/isTypedarray) and returnerror.TypeMismatchon mismatch. ✅ - promise.zig:
rejectnow accepts any DSL/napi value, andrejectWithMessagecreates a proper JS Error object. ✅
LGTM. 🚀
lodekeeper-z
left a comment
There was a problem hiding this comment.
Re-reviewed after fix commits. All previously raised issues addressed:
as*()methods now validate JS types at runtime viaexpectType()✅reject()acceptsanytype, withrejectWithMessage()for proper Error objects ✅exportModulefilters by DSL function signature, validatesjs_classasbool✅- Null-terminated names via
decl.name ++ ""trick ✅ - Typed array subtype validation added ✅
Clean design. LGTM.
|
@lodekeeper-z Please review the last commit and see what implications can there be for adding |
390d398 to
e9e54dd
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…se async model, Object(T) accessors, init contract, wrapFunction error handling, null/undefined distinction Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ucture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use raw C APIs instead of Env.createFunction/defineClass - Make convertArg/callAndConvert/convertReturn pub - Fix invalid unit tests in context.zig - Fix isDslType to use @typeinfo field iteration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure root.zig into napi + js namespaces while preserving backwards-compatible flat exports via usingnamespace. Add thread-local context (env/allocator), and zero-cost DSL wrappers for Number, String, Boolean, BigInt, and Date. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Array, Object(T), Function, TypedArray variants, Promise(T), and Value escape hatch. Update js.zig with full re-exports including all 11 typed array types, createPromise, and throwError helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Array.get() and Function.call() now return !Value (DSL type) instead of !napi.Value
- Promise(T) stores val field alongside deferred; createPromise returns error union
- Value is* methods return bool (using catch return false pattern)
- Value as* methods return error unions
- Added missing Value.asObject(comptime T) method
- throwError uses catch {} instead of catch @Panic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core comptime module that converts DSL-typed Zig functions into N-API C callbacks. Includes isDslType, convertArg, convertReturn, callAndConvert, and wrapFunction which handles error unions, optionals, and plain returns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generates N-API constructor, finalizer, property descriptors, and method wrappers for Zig structs marked with js_class=true. Instance methods are detected by self-param type; static methods use wrapFunction directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Iterates module declarations at comptime, wrapping public functions via wrapFunction and js_class structs via wrapClass, then registers them using raw N-API C calls and napi.module.register. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add wrapFunction, wrapClass, exportModule, and helper functions (isDslType, convertArg, convertReturn, callAndConvert) to the public js module API. Reference new modules in test block. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a class method takes `self: T` (by value), wrapMethod was passing `*T` instead of dereferencing to `T`. Now correctly distinguishes by-value (`T`) from pointer (`*T`, `*const T`) self parameters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add example demonstrating the JS DSL with functions (add, greet, findValue, willThrow) and a Counter class. Update build.zig to build the example as a .node addon. Fix wrapClass property descriptor generation to use a comptime block for Zig 0.14 compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover all DSL types: Number, String, Boolean, BigInt, Date, Array, Object, Function, Value, Uint8Array, Float64Array, Promise, and classes with deinit. Includes error handling, typed objects, callbacks, and mixed DSL + N-API usage sections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 29 tests across 11 describe blocks: basic functions, error handling, primitives (Number, Boolean, String, BigInt, Date), typed objects, arrays, typed arrays, promises, callbacks, Counter/Buffer classes, and mixed DSL + N-API interop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wemeetagain
left a comment
There was a problem hiding this comment.
Great work, this is really nice. Good UX and small, manageable runtime.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file and every example accesses napi types through the inner `.c` namespace (`c.napi_env`, `c.napi_value`, …). Restore the `.c` on the import so call sites keep compiling. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports for every symbol previously namespace-injected from `napi.zig`. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). The `builtin.CallingConvention` union renamed `.C` to `.c`. * `std.Thread.Mutex` → `std.Io.Mutex`. `src/js/class_runtime.zig` kept a small per-class registry behind a mutex; Zig 0.16 moved Mutex into `std.Io` and lock/unlock now require an `Io` argument. Use the stdlib-provided `std.Io.Threaded.global_single_threaded` instance for this hardcoded-reference case (stdlib doc explicitly permits it for non-library code). After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, which Node resolves at runtime when loading the `.node` file) — that's orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file and every example accesses napi types through the inner `.c` namespace (`c.napi_env`, `c.napi_value`, …). Restore the `.c` on the import so call sites keep compiling. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports for every symbol previously namespace-injected from `napi.zig`. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). The `builtin.CallingConvention` union renamed `.C` to `.c`. * `std.Thread.Mutex` → `std.Io.Mutex`. `src/js/class_runtime.zig` kept a small per-class registry behind a mutex; Zig 0.16 moved Mutex into `std.Io` and lock/unlock now require an `Io` argument. - Public API (`registerClass`, `getConstructor`, `materializeClassInstance`) now takes `io: std.Io` as an explicit parameter and uses the cancelable `try mutex.lock(io)`, so callers can propagate cancellation. - `Entry` caches the registering caller's `io`; `cleanupHook` (a napi callback with a fixed `(*Entry)` signature that cannot return errors) reads `entry.io` and uses `lockUncancelable` — unwinding on cancel would leak the entry and leave napi holding a dangling pointer. - `src/js/runtime_io.zig` (new) owns an explicitly-initialized `std.Io.Threaded` instance and exposes `io()`. The two callers of `registerClass`/`materializeClassInstance` that run under napi C callback boundaries (no user-threaded io available) read from this file instead of hard-coding `std.Io.Threaded.global_single_threaded` — keeps the stdlib- policy violation ("library code should avoid that global") confined to a single, self-documented declaration. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, which Node resolves at runtime when loading the `.node` file) — that's orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file and every example accesses napi types through the inner `.c` namespace (`c.napi_env`, `c.napi_value`, …). Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports for every symbol previously namespace-injected from `napi.zig`. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). The `builtin.CallingConvention` union renamed `.C` to `.c`. * `std.Thread.Mutex` → `std.Io.Mutex`. The per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex. Rather than hardcoding `std.Io.Threaded.global_single_threaded` across scattered call sites (stdlib explicitly discourages that in library code), `Env` gains an `io: std.Io` field: - `Env.fromRaw(raw_env)` is the new constructor used by napi C callbacks. It pairs the raw `napi_env` with an `Io` drawn from a single explicitly-initialized `std.Io.Threaded` instance in `src/Env.zig` (`Env.processIo()`). - All 19 direct `Env{ .env = raw_env }` construction sites (in `module.zig`, `callback.zig`, `finalize_callback.zig`, `async_work.zig`, `threadsafe_function.zig`, `wrap_function.zig`, `wrap_class.zig`) switched to `Env.fromRaw`, so every callback path carries `io` alongside the env. - `class_runtime.zig`'s mutex now uses `env.io` directly. Public APIs (`registerClass`, `getConstructor`, `materializeClassInstance`) take `env` (which already carries `io`) — no separate `io` parameter. - Normal paths use cancelable `try mutex.lock(env.io)` so caller cancellation can propagate. - `Entry` caches `env.io` so `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) can still operate the shared mutex. That path uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and leave napi holding a dangling pointer. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, which Node resolves at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file and every example accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. Rather than reaching for stdlib's `global_single_threaded` directly at every call site, introduce `src/runtime.zig`: - `pub fn init(io: std.Io) void` — caller-settable; intended for the user's moduleInit to inject their application's `Io`. - `pub fn io() std.Io` — returns the configured `Io`, or a default zapi-owned `init_single_threaded` instance if `init` was never called, so simple modules keep working without extra setup. `Env` stays a pure napi-env wrapper (no `io` field). Every napi C callback still uses `Env.fromRaw(raw_env)` (added so future additions to `Env` don't require touching 20+ call sites), and `class_runtime.zig`'s mutex reads `runtime.io()` directly: - `registerClass`/`getConstructor` use cancelable `try mutex.lock()` so caller cancellation propagates. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
The merge with main brought in the js_dsl feature (ChainSafe#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration.
* chore: upgrade to Zig 0.16.0-dev (master) - Remove usingnamespace (removed in 0.15): c.zig now exports cImport as .c field - All imports updated: @import("c.zig") → @import("c.zig").c - callconv(.C) → callconv(.c) (lowercase) - napi module requires link_libc = true for @cImport - Example disabled (TODO: std.time.Timer/sleep moved to std.Io) 🤖 Generated with AI assistance * chore: merge upstream main (Ref refactor, raw variant functions, safer callbacks) Updates examples to match new wrap() signature from #10 (added ref parameter). 🤖 Generated with AI assistance * fix: restore hello_world example with Zig 0.16 Timer migration - Replace std.time.Timer with local Timer using std.c.clock_gettime - Replace std.time.sleep with std.c.nanosleep - Restore hello_world module, library, and test in build.zig - Fix b.modules.put to use b.allocator (Zig 0.16 API change) - Update minimum_zig_version to 0.16.0 - Update CI setup-env to use Zig 0.16.0 - Remove duplicate .zig-cache/ in .gitignore * refactor(examples): use std.Io instead of libc for timer and sleep Replace std.c.clock_gettime/nanosleep with std.Io.Timestamp and std.Io.sleep, using Io.Threaded.init_single_threaded for the Io instance in the NAPI callback context. * refactor(examples): use Io.Threaded.global_single_threaded Remove module-level var and getIo() helper. Use the stdlib-provided global_single_threaded instance directly. * refactor(examples): use explicit Io.Threaded init instead of global Per Zig author recommendation, prefer explicit one-time initialization over global_single_threaded for dynamically-loaded NAPI libraries. * refactor(examples): remove Timer wrapper, use Io.Timestamp directly The custom Timer struct was an unnecessary wrapper around Io.Timestamp. Use Io.Timestamp directly in the NAPI callbacks. * fix(build): exclude example tests from global test step Example modules reference NAPI symbols that are only available at runtime via Node.js. Running them as standalone zig tests causes linker errors. Keep them as individual test steps (test:example_hello_world, test:example_type_tag) but exclude from the global `zig build test`. * fix: address PR review comments - Remove stale "generated by zbuild" comment from build.zig.zon - Update zbuild.zon minimum_zig_version to 0.16.0 - Add link_libc to zapi module in zbuild.zon for consistency with build.zig * fix: use threadlocal for Io.Threaded to avoid cross-thread sharing Each thread (main, libuv worker, spawned) gets its own init_single_threaded instance, which is correct since init_single_threaded has no synchronization. * fix: restore zbuild generated-file headers build.zig and build.zig.zon are generated by zbuild — keep the header so manual edits don't get silently overwritten on regeneration. * fix: remove unnecessary link_libc from zapi module zapi only uses @cImport for NAPI C headers (stddef.h, stdint.h) which Zig provides natively. link_libc is only needed by the example dynamic libraries that load into Node.js at runtime. * example: use stdlib global_single_threaded Io in hello_world The threadlocal `init_single_threaded` instance was defensively scoped per thread on the theory that cross-thread sharing of a single-threaded Threaded could race. In practice stdlib already provides `std.Io.Threaded.global_single_threaded` for exactly this scenario: > In some cases such as debugging, it is desirable to hardcode a > reference to this `Io` implementation. It's a single process-wide instance, and — importantly — the only ops this example uses (`Timestamp.now`, `random`, etc.) bottom out in OS calls that don't touch the `Threaded` struct's internal mutable state, so one shared instance is safe for this use case. This removes the `threadlocal var` declaration entirely, matching the stdlib-recommended pattern for examples. * chore: port main's newly-merged code to Zig 0.16 The merge with main brought in the js_dsl feature (#11) and a refactor of `src/root.zig` (napi symbols moved to `src/napi.zig`, re-exported via `pub usingnamespace`). All of that was written for 0.14.1 and hits several 0.16 breaking changes: * `b.modules.put(key, value)` → `b.modules.put(allocator, key, value)`. Two call sites in `build.zig` were missed by the previous update pass (the `napi` module and the `example_js_dsl` module). * `src/napi.zig` exported `pub const c = @import("c.zig")`, but every internal file accesses napi types through the inner `.c` namespace. Restore the `.c` on the import. * `pub usingnamespace` was removed in Zig 0.16. Replace it in `src/root.zig` with explicit `pub const foo = napi.foo;` re-exports. * `callconv(.C)` → `callconv(.c)` (6 NAPI callback shims in `src/js/wrap_class.zig` and `src/js/wrap_function.zig`). * `std.Thread.Mutex` → `std.Io.Mutex`. The DSL's per-class registry in `src/js/class_runtime.zig` needs a 0.16-compatible mutex, which requires an `Io`. The `Io` is threaded in through the DSL's entry point rather than letting zapi carry global state: - `js.exportModule` gains an optional `.io = fn () std.Io` field in `options`. The generated `moduleInit` calls it after the user's `options.init` hook and passes the result down into `registerDecls` → `class_runtime.registerClass`. If `.io` is omitted, a DSL-local `init_single_threaded` fallback keeps zero-config modules compiling. - `registerClass` caches the resolved `Io` in per-`T` `state(T).io`; `getConstructor`, `materializeClassInstance`, and `cleanupHook` all read from this cache because they execute inside napi C callbacks that can't receive user-provided `Io`. - Normal paths (`registerClass`, `getConstructor`) use cancelable `try mutex.lock(io)` so caller cancellation can propagate. - `cleanupHook` (a napi C callback with a fixed `(*Entry)` signature that cannot return errors) uses `lockUncancelable` deliberately — unwinding on cancel would leak the entry and hand napi a dangling pointer. * `Env` gains a `fromRaw(raw_env)` constructor so the 19 napi C callback sites don't all hand-roll the `Env{ .env = ... }` literal. `Env` itself stays a pure `napi_env` wrapper — no `io` field. After this commit `zig build` succeeds, and `zig build test` runs the 42 regular tests green. `example_js_dsl` has a separate, pre-existing link issue in `zig build test` (undefined `_napi_wrap` and friends, resolved by Node at runtime when loading the `.node` file) — orthogonal to the 0.16 migration. * chore: migrate to zbuild library mode Replace the auto-generated 175-line build.zig + the deleted zbuild.zon CLI mode with the new zbuild library-mode pattern: a 6-line wrapper invoking `zbuild.configureBuild` over a manifest-driven build.zig.zon. build.zig: - shrunk from 175 lines to a wrapper that delegates to `zbuild.configureBuild(b, @import("build.zig.zon"), .{})`. - Post-processes the 3 example test artifacts to set `linker_allow_shlib_undefined = true` (zbuild exposes this option only for libraries, so we apply it post-hoc to tests that link against napi C symbols Node provides at dlopen time). build.zig.zon: - Adds zbuild as a dependency (pinned to refactor/comptime-library-rewrite @ 17c389b — same pin as lodestar-z `main`). - Declares all modules / libraries / tests / options_modules in the manifest; zbuild materializes them at build time. - All build steps from the previous build.zig are preserved (`build-lib:*`, `test:*`, `build-test:*`, `test`). Verified: `zig build` clean, `zig build test` passes 42/42, all 3 `.node` files install correctly, `zig fmt --check` clean. * chore: declare test linker_allow_shlib_undefined in manifest Bump zbuild to 7ec852f, which adds `linker_allow_shlib_undefined` support for `tests` entries (previously only allowed in `libraries`). This lets the example tests declare the flag directly in the manifest instead of post-processing the test artifacts in build.zig: build.zig: shrinks back to a 6-line wrapper (no post-hoc fixup loop). build.zig.zon: 3 example test entries each set `linker_allow_shlib_undefined = true`. * fix: remove obsolete zbuild manifest --------- Co-authored-by: lodekeeper-z <lodekeeper-z@users.noreply.github.com> Co-authored-by: Chen Kai <281165273grape@gmail.com> Co-authored-by: Chen Kai <grapebabamarch@outlook.com>
Summary
This PR upgrades the JS DSL from marker-based class exports to a typed class metadata model and adds automatic JS instance
materialization for exported class returns.
Highlights
pub const js_meta = js.class(.{ ... })js.prop(.{ .get = ..., .set = ... })js.Valueas the escape hatch for dynamic/heterogeneous JS interopundefinedfor optional DSL argumentsu64values abovei64max inNumber.fromExamples
examples/js_dslto the newjs_metapatternNotes
build.zigis generated and was regenerated fromzbuild.zonmainVerification
zbuild syncpnpm test