zignapi 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { parseArgs } from "node:util";
2
- import { existsSync, copyFileSync, readdirSync } from "node:fs";
2
+ import { existsSync, copyFileSync, readdirSync, writeFileSync } from "node:fs";
3
3
  import { join, basename, extname } from "node:path";
4
+ import { createRequire } from "node:module";
4
5
  import process from "node:process";
5
- import { runZig } from "./zig.js";
6
+ import { runZig, errMessage } from "./zig.js";
6
7
  const BUILD_HELP = `zignapi build — build the addon in the current directory
7
8
 
8
9
  Usage:
@@ -12,8 +13,9 @@ Options:
12
13
  --release Optimize the build (-Doptimize=ReleaseFast)
13
14
  -h, --help Show this help
14
15
 
15
- Runs "zig build" in the current directory, then copies the resulting shared
16
- library to ./<name>.node (renaming a plain .dylib/.so/.dll if needed).
16
+ Runs "zig build" in the current directory, copies the resulting shared library
17
+ to ./<name>.node (renaming a plain .dylib/.so/.dll if needed), then generates
18
+ index.js and index.d.ts from the addon's embedded type declarations.
17
19
  `;
18
20
  export async function runBuild(argv) {
19
21
  const { values } = parseArgs({
@@ -36,13 +38,42 @@ export async function runBuild(argv) {
36
38
  zigArgs.push("-Doptimize=ReleaseFast");
37
39
  runZig(cwd, zigArgs, { repairZon: join(cwd, "build.zig.zon") });
38
40
  // Locate the freshly built addon and copy it to ./<name>.node.
39
- const addon = findAddon(join(cwd, "zig-out"));
40
- if (!addon) {
41
+ const addonPath = findAddon(join(cwd, "zig-out"));
42
+ if (!addonPath) {
41
43
  throw new Error("build succeeded but no addon (.node or shared library) was found under zig-out");
42
44
  }
43
- const dest = join(cwd, addonName(addon));
44
- copyFileSync(addon, dest);
45
- process.stdout.write(`✔ built ${basename(dest)}\n`);
45
+ const nodeFile = addonName(addonPath);
46
+ copyFileSync(addonPath, join(cwd, nodeFile));
47
+ process.stdout.write(`✔ built ${nodeFile}\n`);
48
+ generateBindings(cwd, nodeFile);
49
+ }
50
+ /**
51
+ * Emit `index.js` (a loader that re-exports the addon) and `index.d.ts` (the
52
+ * TypeScript declarations) from the type info the addon embeds as
53
+ * `__zignapi_dts__`. Non-fatal: a build that can't be introspected still counts
54
+ * as a successful build.
55
+ */
56
+ function generateBindings(cwd, nodeFile) {
57
+ const require = createRequire(import.meta.url);
58
+ let addon;
59
+ try {
60
+ addon = require(join(cwd, nodeFile));
61
+ }
62
+ catch (err) {
63
+ process.stderr.write(`note: skipped type generation (could not load ${nodeFile}: ${errMessage(err)})\n`);
64
+ return;
65
+ }
66
+ const dts = addon.__zignapi_dts__;
67
+ if (typeof dts !== "string" || dts.length === 0) {
68
+ process.stderr.write("note: addon has no embedded type info; skipping index.d.ts\n");
69
+ return;
70
+ }
71
+ writeFileSync(join(cwd, "index.d.ts"), `// Generated by zignapi — do not edit.\n\n${dts}`);
72
+ writeFileSync(join(cwd, "index.js"), `"use strict";\n` +
73
+ `const addon = require("./${nodeFile}");\n` +
74
+ `const { __zignapi_dts__, ...api } = addon;\n` +
75
+ `module.exports = api;\n`);
76
+ process.stdout.write("✔ generated index.js + index.d.ts\n");
46
77
  }
47
78
  /**
48
79
  * Find the built addon under zig-out. Prefers an already-named `.node`,
package/dist/new.js CHANGED
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from
3
3
  import { join, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import process from "node:process";
6
- import { releaseUrl, resolveZignapiSources, runZig } from "./zig.js";
6
+ import { releaseUrl, resolveZignapiSources, runZig, errMessage } from "./zig.js";
7
7
  // The template tree lives at the package root (as `templates/`), so it resolves
8
8
  // the same whether we run from the compiled `dist/` or the source `src/`.
9
9
  const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
@@ -113,6 +113,3 @@ function validateName(name) {
113
113
  `must not start with a digit)`);
114
114
  }
115
115
  }
116
- function errMessage(err) {
117
- return err instanceof Error ? err.message : String(err);
118
- }
package/dist/zig.js CHANGED
@@ -64,6 +64,10 @@ export function runZig(cwd, args, { repairZon } = {}) {
64
64
  }
65
65
  return res;
66
66
  }
67
+ /** Extract a human-readable message from an unknown thrown value. */
68
+ export function errMessage(err) {
69
+ return err instanceof Error ? err.message : String(err);
70
+ }
67
71
  /** Insert or replace the `.fingerprint` field in a build.zig.zon. */
68
72
  export function patchFingerprint(zonPath, value) {
69
73
  if (!existsSync(zonPath))
package/native/build.zig CHANGED
@@ -42,17 +42,18 @@ pub fn build(b: *std.Build) void {
42
42
  });
43
43
  b.getInstallStep().dependOn(&check_obj.step);
44
44
 
45
- // Unit tests: only the pure-comptime conversions (no N-API runtime calls),
46
- // so the test binary links without Node present.
47
- const tests_mod = b.createModule(.{
48
- .root_source_file = b.path("src/convert.zig"),
49
- .target = target,
50
- .optimize = optimize,
51
- .link_libc = true,
52
- });
53
- tests_mod.addIncludePath(headers);
54
- const unit_tests = b.addTest(.{ .root_module = tests_mod });
55
- const run_tests = b.addRunArtifact(unit_tests);
45
+ // Unit tests: only pure-comptime code (no N-API runtime calls), so the test
46
+ // binaries link without Node present.
56
47
  const test_step = b.step("test", "Run zignapi unit tests");
57
- test_step.dependOn(&run_tests.step);
48
+ for ([_][]const u8{ "src/convert.zig", "src/typedefs.zig" }) |root| {
49
+ const tests_mod = b.createModule(.{
50
+ .root_source_file = b.path(root),
51
+ .target = target,
52
+ .optimize = optimize,
53
+ .link_libc = true,
54
+ });
55
+ tests_mod.addIncludePath(headers);
56
+ const unit_tests = b.addTest(.{ .root_module = tests_mod });
57
+ test_step.dependOn(&b.addRunArtifact(unit_tests).step);
58
+ }
58
59
  }
@@ -1,6 +1,6 @@
1
1
  .{
2
2
  .name = .zignapi,
3
- .version = "0.1.1",
3
+ .version = "0.2.0",
4
4
  .fingerprint = 0x582024f92c57df77, // Changing this has security and trust implications.
5
5
  .minimum_zig_version = "0.16.0",
6
6
  .dependencies = .{},
@@ -60,3 +60,10 @@ pub fn createFunction(env: Env, name: [:0]const u8, cb: Callback) Error!Value {
60
60
  try check(c.napi_create_function(env, name.ptr, name.len, cb, null, &result));
61
61
  return result;
62
62
  }
63
+
64
+ /// Create a JS UTF-8 string from a Zig slice.
65
+ pub fn createString(env: Env, s: []const u8) Error!Value {
66
+ var result: Value = undefined;
67
+ try check(c.napi_create_string_utf8(env, s.ptr, s.len, &result));
68
+ return result;
69
+ }
@@ -12,6 +12,7 @@
12
12
  const std = @import("std");
13
13
  const napi = @import("napi.zig");
14
14
  const convert = @import("convert.zig");
15
+ const typedefs = @import("typedefs.zig");
15
16
  const c = napi.c;
16
17
 
17
18
  /// Export `napi_register_module_v1` for the given set of functions.
@@ -27,6 +28,10 @@ pub fn register(comptime defs: anytype) void {
27
28
  else => @compileError("zignapi.register expects a struct literal, e.g. .{ .add = add }"),
28
29
  }
29
30
 
31
+ // TypeScript declarations for this module, generated at comptime and
32
+ // embedded so `zignapi build` can emit `index.d.ts` / `index.js`.
33
+ const dts = typedefs.declarations(defs);
34
+
30
35
  const Registrar = struct {
31
36
  fn entry(env: napi.Env, exports: napi.Value) callconv(.c) napi.Value {
32
37
  inline for (@typeInfo(Defs).@"struct".fields) |field| {
@@ -36,6 +41,11 @@ pub fn register(comptime defs: anytype) void {
36
41
  return null;
37
42
  };
38
43
  }
44
+ // Best-effort: attach the type declarations. Failure here must not
45
+ // break a working module, so ignore errors.
46
+ if (napi.createString(env, dts)) |v| {
47
+ napi.setNamedProperty(env, exports, "__zignapi_dts__", v) catch {};
48
+ } else |_| {}
39
49
  return exports;
40
50
  }
41
51
  };
@@ -0,0 +1,77 @@
1
+ //! Comptime generation of TypeScript declarations for the registered functions.
2
+ //!
3
+ //! The mapping reuses `convert.classify`, so the `.d.ts` and the runtime
4
+ //! conversions can never drift apart. `register.zig` embeds the produced string
5
+ //! in the addon (as the `__zignapi_dts__` export); `zignapi build` reads it back
6
+ //! and writes `index.d.ts` / `index.js`.
7
+
8
+ const std = @import("std");
9
+ const convert = @import("convert.zig");
10
+
11
+ /// The TypeScript type string for a Zig type. Error unions map to their payload
12
+ /// (Zig errors surface as thrown JS exceptions, which don't appear in the type);
13
+ /// `void` maps to `void`.
14
+ pub fn tsType(comptime T: type) []const u8 {
15
+ return switch (@typeInfo(T)) {
16
+ .void => "void",
17
+ .error_union => |eu| tsType(eu.payload),
18
+ else => switch (convert.classify(T)) {
19
+ .int, .float => "number",
20
+ .bool => "boolean",
21
+ .string => "string",
22
+ .unsupported => @compileError(
23
+ "zignapi: no TypeScript mapping for '" ++ @typeName(T) ++ "'",
24
+ ),
25
+ },
26
+ };
27
+ }
28
+
29
+ /// Build the `.d.ts` body: one `export function` line per registered function.
30
+ /// Parameter names aren't available from `@typeInfo`, so they're `arg0`, `arg1`, …
31
+ pub fn declarations(comptime defs: anytype) []const u8 {
32
+ comptime var out: []const u8 = "";
33
+ inline for (@typeInfo(@TypeOf(defs)).@"struct".fields) |field| {
34
+ const fn_info = @typeInfo(@TypeOf(@field(defs, field.name))).@"fn";
35
+ comptime var params: []const u8 = "";
36
+ inline for (fn_info.params, 0..) |param, i| {
37
+ const sep = if (i == 0) "" else ", ";
38
+ params = params ++ sep ++ std.fmt.comptimePrint(
39
+ "arg{d}: {s}",
40
+ .{ i, tsType(param.type.?) },
41
+ );
42
+ }
43
+ out = out ++ "export function " ++ field.name ++ "(" ++ params ++
44
+ "): " ++ tsType(fn_info.return_type.?) ++ ";\n";
45
+ }
46
+ return out;
47
+ }
48
+
49
+ test "declarations render one export per function" {
50
+ const S = struct {
51
+ fn add(a: i32, b: i32) i32 {
52
+ return a + b;
53
+ }
54
+ fn greet(name: []const u8) []const u8 {
55
+ return name;
56
+ }
57
+ fn toggle(x: bool) bool {
58
+ return !x;
59
+ }
60
+ fn checked(x: i32) !i32 {
61
+ return x;
62
+ }
63
+ };
64
+ const dts = comptime declarations(.{
65
+ .add = S.add,
66
+ .greet = S.greet,
67
+ .toggle = S.toggle,
68
+ .checked = S.checked,
69
+ });
70
+ try std.testing.expectEqualStrings(
71
+ \\export function add(arg0: number, arg1: number): number;
72
+ \\export function greet(arg0: string): string;
73
+ \\export function toggle(arg0: boolean): boolean;
74
+ \\export function checked(arg0: number): number;
75
+ \\
76
+ , dts);
77
+ }
@@ -12,6 +12,7 @@ const std = @import("std");
12
12
 
13
13
  pub const napi = @import("napi.zig");
14
14
  pub const convert = @import("convert.zig");
15
+ pub const typedefs = @import("typedefs.zig");
15
16
  pub const async_ = @import("async.zig");
16
17
 
17
18
  /// Register Zig functions as a Node addon. See `register.zig`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zignapi",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "CLI to scaffold and build native Node.js addons written in Zig (the `zignapi` command)",
5
5
  "keywords": [
6
6
  "zig",
@@ -2,3 +2,7 @@ node_modules/
2
2
  zig-out/
3
3
  .zig-cache/
4
4
  *.node
5
+
6
+ # Generated by `zignapi build`
7
+ index.js
8
+ index.d.ts
@@ -2,6 +2,8 @@
2
2
  "name": "__NAME__",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
5
7
  "scripts": {
6
8
  "build": "zignapi build",
7
9
  "test": "node --test"
package/templates/test.js CHANGED
@@ -1,7 +1,10 @@
1
1
  const test = require("node:test");
2
2
  const assert = require("node:assert");
3
+ const fs = require("node:fs");
3
4
 
4
- const addon = require("./zig-out/lib/__NAME__.node");
5
+ // The generated loader (index.js) re-exports the addon; index.d.ts gives it types.
6
+ // Both are produced by `zignapi build`, so build before running the tests.
7
+ const addon = require("./index.js");
5
8
 
6
9
  test("add(2, 3) === 5", () => {
7
10
  assert.strictEqual(addon.add(2, 3), 5);
@@ -10,3 +13,9 @@ test("add(2, 3) === 5", () => {
10
13
  test("greet returns a string", () => {
11
14
  assert.strictEqual(addon.greet("world"), "hello from Zig");
12
15
  });
16
+
17
+ test("index.d.ts declares the exports", () => {
18
+ const dts = fs.readFileSync("./index.d.ts", "utf8");
19
+ assert.match(dts, /export function add\(arg0: number, arg1: number\): number;/);
20
+ assert.match(dts, /export function greet\(arg0: string\): string;/);
21
+ });