zignapi 0.1.0 → 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/README.md +2 -2
- package/dist/build.js +40 -9
- package/dist/new.js +13 -16
- package/dist/zig.js +12 -8
- package/native/build.zig +20 -19
- package/native/build.zig.zon +3 -3
- package/native/src/_check.zig +4 -4
- package/native/src/convert.zig +1 -1
- package/native/src/napi.zig +7 -0
- package/native/src/register.zig +17 -7
- package/native/src/typedefs.zig +77 -0
- package/native/src/{zigbind.zig → zignapi.zig} +3 -2
- package/package.json +5 -5
- package/templates/README.md +3 -3
- package/templates/build.zig +3 -3
- package/templates/build.zig.zon +2 -2
- package/templates/dot-gitignore +4 -0
- package/templates/package.json +2 -0
- package/templates/src/main.zig +2 -2
- package/templates/test.js +10 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# zignapi
|
|
2
2
|
|
|
3
|
-
The CLI for [
|
|
3
|
+
The CLI for [zignapi](https://github.com/zignapi/zignapi): scaffold and build
|
|
4
4
|
native Node.js addons written in Zig. Written in TypeScript, compiled to ESM with
|
|
5
5
|
**zero runtime dependencies**, no `node-gyp`. Requires **Zig 0.16.0** on `PATH`
|
|
6
6
|
and **Node >= 18**.
|
|
@@ -17,7 +17,7 @@ zignapi build [--release] # run `zig build` in the cwd, emit ./<name>.node
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
- `new` copies the template tree (substituting the project name) and pins the
|
|
20
|
-
`
|
|
20
|
+
`zignapi` Zig dependency with `zig fetch --save`.
|
|
21
21
|
- `build` runs `zig build` (adding `-Doptimize=ReleaseFast` with `--release`),
|
|
22
22
|
then copies the produced shared library to `./<name>.node`. If `build.zig.zon`
|
|
23
23
|
is missing a fingerprint, it fills in the value Zig suggests and retries.
|
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,
|
|
16
|
-
|
|
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
|
|
40
|
-
if (!
|
|
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
|
|
44
|
-
copyFileSync(
|
|
45
|
-
process.stdout.write(`✔ built ${
|
|
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,
|
|
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");
|
|
@@ -14,7 +14,7 @@ Usage:
|
|
|
14
14
|
zignapi new <name> [--local]
|
|
15
15
|
|
|
16
16
|
Creates ./<name> from the built-in template, substituting the project name,
|
|
17
|
-
then wires up the
|
|
17
|
+
then wires up the zignapi Zig dependency with "zig fetch --save" (pinned by
|
|
18
18
|
content hash). By default it fetches the hosted release tarball, so the project
|
|
19
19
|
is portable across machines/CI; if that's unreachable it falls back to the Zig
|
|
20
20
|
sources bundled with the CLI.
|
|
@@ -46,7 +46,7 @@ export async function runNew(argv) {
|
|
|
46
46
|
throw new Error(`directory '${name}' already exists`);
|
|
47
47
|
}
|
|
48
48
|
copyTemplate(TEMPLATES_DIR, target, name);
|
|
49
|
-
|
|
49
|
+
addZignapiDependency(target, { local: values.local ?? false });
|
|
50
50
|
process.stdout.write(`✔ created ${name}/\n\n` +
|
|
51
51
|
`Next steps:\n` +
|
|
52
52
|
` cd ${name}\n` +
|
|
@@ -54,36 +54,36 @@ export async function runNew(argv) {
|
|
|
54
54
|
` node --test\n`);
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* Add the `
|
|
57
|
+
* Add the `zignapi` Zig dependency to the freshly scaffolded project via
|
|
58
58
|
* `zig fetch --save`, pinning it by content hash. Prefers the hosted release
|
|
59
59
|
* tarball (portable across machines) and falls back to the Zig sources bundled
|
|
60
60
|
* with the CLI if that's unreachable or `local` is set. Non-fatal: if Zig isn't
|
|
61
61
|
* available at all, tell the user how to finish the wiring later.
|
|
62
62
|
*/
|
|
63
|
-
function
|
|
63
|
+
function addZignapiDependency(target, { local }) {
|
|
64
64
|
const repairZon = join(target, "build.zig.zon");
|
|
65
65
|
if (!local) {
|
|
66
66
|
const url = releaseUrl();
|
|
67
67
|
try {
|
|
68
|
-
runZig(target, ["fetch", "--save=
|
|
69
|
-
process.stdout.write(`✔ added
|
|
68
|
+
runZig(target, ["fetch", "--save=zignapi", url], { repairZon });
|
|
69
|
+
process.stdout.write(`✔ added zignapi dependency from ${url}\n`);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
catch (err) {
|
|
73
|
-
process.stderr.write(`note: could not fetch the hosted
|
|
73
|
+
process.stderr.write(`note: could not fetch the hosted zignapi release (${errMessage(err)}); ` +
|
|
74
74
|
`falling back to the bundled sources.\n`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
let sources;
|
|
78
78
|
try {
|
|
79
|
-
sources =
|
|
80
|
-
runZig(target, ["fetch", "--save=
|
|
81
|
-
process.stdout.write("✔ added
|
|
79
|
+
sources = resolveZignapiSources();
|
|
80
|
+
runZig(target, ["fetch", "--save=zignapi", sources], { repairZon });
|
|
81
|
+
process.stdout.write("✔ added zignapi dependency from bundled sources\n");
|
|
82
82
|
}
|
|
83
83
|
catch (err) {
|
|
84
|
-
process.stderr.write(`warning: could not add the
|
|
84
|
+
process.stderr.write(`warning: could not add the zignapi dependency automatically ` +
|
|
85
85
|
`(${errMessage(err)}).\nRun this inside the project once Zig 0.16.0 is ` +
|
|
86
|
-
`available:\n zig fetch --save=
|
|
86
|
+
`available:\n zig fetch --save=zignapi ${sources ?? releaseUrl()}\n`);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
@@ -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
|
@@ -7,25 +7,25 @@ import process from "node:process";
|
|
|
7
7
|
// up is the package root.
|
|
8
8
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
9
|
/**
|
|
10
|
-
* The hosted release tarball of the
|
|
11
|
-
* (portable, shareable) dependency source. Overridable via
|
|
10
|
+
* The hosted release tarball of the zignapi Zig package, used as the default
|
|
11
|
+
* (portable, shareable) dependency source. Overridable via ZIGNAPI_RELEASE_URL
|
|
12
12
|
* — handy for testing against a local `file://` tarball or a mirror. The
|
|
13
13
|
* version is taken from this CLI's package.json and must match the release tag.
|
|
14
14
|
*/
|
|
15
15
|
export function releaseUrl() {
|
|
16
|
-
const override = process.env.
|
|
16
|
+
const override = process.env.ZIGNAPI_RELEASE_URL;
|
|
17
17
|
if (override)
|
|
18
18
|
return override;
|
|
19
19
|
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
20
20
|
const v = pkg.version;
|
|
21
|
-
return `https://github.com/
|
|
21
|
+
return `https://github.com/zignapi/zignapi/releases/download/v${v}/zignapi-${v}.tar.gz`;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Locate the
|
|
24
|
+
* Locate the zignapi Zig sources (the `native/` directory) that ship with this
|
|
25
25
|
* CLI. Works both from the published npm package (bundled at `<pkg>/native`)
|
|
26
|
-
* and from the monorepo checkout (`packages/
|
|
26
|
+
* and from the monorepo checkout (`packages/zignapi` -> `../../native`).
|
|
27
27
|
*/
|
|
28
|
-
export function
|
|
28
|
+
export function resolveZignapiSources() {
|
|
29
29
|
const candidates = [
|
|
30
30
|
join(PKG_ROOT, "native"), // bundled inside the published package
|
|
31
31
|
join(PKG_ROOT, "..", "..", "native"), // monorepo layout
|
|
@@ -34,7 +34,7 @@ export function resolveZigbindSources() {
|
|
|
34
34
|
if (existsSync(join(dir, "build.zig.zon")))
|
|
35
35
|
return dir;
|
|
36
36
|
}
|
|
37
|
-
throw new Error("could not locate the
|
|
37
|
+
throw new Error("could not locate the zignapi Zig sources (native/) shipped with the CLI");
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Run `zig` with the given args in `cwd`. If the run fails because
|
|
@@ -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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
|
|
3
|
-
/// Build script for the `
|
|
3
|
+
/// Build script for the `zignapi` Zig library.
|
|
4
4
|
///
|
|
5
5
|
/// This is a Zig package, not an npm package. Its main product is the Zig
|
|
6
|
-
/// module named "
|
|
6
|
+
/// module named "zignapi" (exposed via `b.addModule`), which addons import to
|
|
7
7
|
/// register their functions with Node. Building here also runs two local
|
|
8
8
|
/// checks so `zig build` in this directory validates the library on its own:
|
|
9
9
|
///
|
|
@@ -16,12 +16,12 @@ pub fn build(b: *std.Build) void {
|
|
|
16
16
|
|
|
17
17
|
const headers = b.path("vendor/node-api-headers");
|
|
18
18
|
|
|
19
|
-
// The module consumers import as `@import("
|
|
19
|
+
// The module consumers import as `@import("zignapi")`. It links libc (for
|
|
20
20
|
// the C allocator used to marshal string arguments, and to make the N-API
|
|
21
21
|
// headers available to `@cImport`). Target/optimize are left unset here so
|
|
22
22
|
// each consumer picks them; the include path travels with the module.
|
|
23
|
-
const mod = b.addModule("
|
|
24
|
-
.root_source_file = b.path("src/
|
|
23
|
+
const mod = b.addModule("zignapi", .{
|
|
24
|
+
.root_source_file = b.path("src/zignapi.zig"),
|
|
25
25
|
.link_libc = true,
|
|
26
26
|
});
|
|
27
27
|
mod.addIncludePath(headers);
|
|
@@ -37,22 +37,23 @@ pub fn build(b: *std.Build) void {
|
|
|
37
37
|
});
|
|
38
38
|
check_mod.addIncludePath(headers);
|
|
39
39
|
const check_obj = b.addObject(.{
|
|
40
|
-
.name = "
|
|
40
|
+
.name = "zignapi-check",
|
|
41
41
|
.root_module = check_mod,
|
|
42
42
|
});
|
|
43
43
|
b.getInstallStep().dependOn(&check_obj.step);
|
|
44
44
|
|
|
45
|
-
// Unit tests: only
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
// Unit tests: only pure-comptime code (no N-API runtime calls), so the test
|
|
46
|
+
// binaries link without Node present.
|
|
47
|
+
const test_step = b.step("test", "Run zignapi unit tests");
|
|
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
|
}
|
package/native/build.zig.zon
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.{
|
|
2
|
-
.name = .
|
|
3
|
-
.version = "0.
|
|
4
|
-
.fingerprint =
|
|
2
|
+
.name = .zignapi,
|
|
3
|
+
.version = "0.2.0",
|
|
4
|
+
.fingerprint = 0x582024f92c57df77, // Changing this has security and trust implications.
|
|
5
5
|
.minimum_zig_version = "0.16.0",
|
|
6
6
|
.dependencies = .{},
|
|
7
7
|
.paths = .{
|
package/native/src/_check.zig
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
//! Build-only compile check (never shipped to consumers, not imported by
|
|
2
|
-
//! `
|
|
2
|
+
//! `zignapi.zig`). Building this as an *object* forces the whole comptime
|
|
3
3
|
//! pipeline — `register` → the per-function callback trampolines → `convert`
|
|
4
4
|
//! for every supported type, including an error union — to be analyzed, so
|
|
5
5
|
//! `zig build` in `native/` fails loudly on any API or type mistake. The
|
|
6
6
|
//! N-API symbols it references stay undefined in the object file; they are
|
|
7
7
|
//! resolved when a real addon links, exactly as in production.
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const napi =
|
|
9
|
+
const zignapi = @import("zignapi.zig");
|
|
10
|
+
const napi = zignapi.napi;
|
|
11
11
|
|
|
12
12
|
// Reference the concrete (non-generic) wrappers so they are compiled too.
|
|
13
13
|
comptime {
|
|
@@ -35,7 +35,7 @@ fn checked(x: i32) !i32 {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
comptime {
|
|
38
|
-
|
|
38
|
+
zignapi.register(.{
|
|
39
39
|
.add = add,
|
|
40
40
|
.negate = negate,
|
|
41
41
|
.scale = scale,
|
package/native/src/convert.zig
CHANGED
|
@@ -31,7 +31,7 @@ pub fn classify(comptime T: type) Kind {
|
|
|
31
31
|
/// Compile-time guard that fails with a readable message for unsupported types.
|
|
32
32
|
fn assertSupported(comptime T: type) void {
|
|
33
33
|
if (classify(T) == .unsupported) {
|
|
34
|
-
@compileError("
|
|
34
|
+
@compileError("zignapi: unsupported type '" ++ @typeName(T) ++
|
|
35
35
|
"' (supported: integers, floats, bool, []const u8)");
|
|
36
36
|
}
|
|
37
37
|
}
|
package/native/src/napi.zig
CHANGED
|
@@ -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
|
+
}
|
package/native/src/register.zig
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
//! Usage from an addon's root source file:
|
|
5
5
|
//!
|
|
6
6
|
//! ```zig
|
|
7
|
-
//! const
|
|
7
|
+
//! const zignapi = @import("zignapi");
|
|
8
8
|
//! pub fn add(a: i32, b: i32) i32 { return a + b; }
|
|
9
|
-
//! comptime {
|
|
9
|
+
//! comptime { zignapi.register(.{ .add = add }); }
|
|
10
10
|
//! ```
|
|
11
11
|
|
|
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.
|
|
@@ -24,18 +25,27 @@ pub fn register(comptime defs: anytype) void {
|
|
|
24
25
|
const Defs = @TypeOf(defs);
|
|
25
26
|
switch (@typeInfo(Defs)) {
|
|
26
27
|
.@"struct" => {},
|
|
27
|
-
else => @compileError("
|
|
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| {
|
|
33
38
|
const func = @field(defs, field.name);
|
|
34
39
|
registerOne(env, exports, field.name, func) catch {
|
|
35
|
-
napi.throwError(env, "
|
|
40
|
+
napi.throwError(env, "zignapi: failed to register '" ++ field.name ++ "'");
|
|
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
|
};
|
|
@@ -67,7 +77,7 @@ fn Wrap(comptime func: anytype) type {
|
|
|
67
77
|
// Collect the JS arguments (one slot per Zig parameter).
|
|
68
78
|
var argv: [fn_info.params.len]napi.Value = undefined;
|
|
69
79
|
_ = napi.getCallbackInfo(env, info, &argv) catch {
|
|
70
|
-
napi.throwError(env, "
|
|
80
|
+
napi.throwError(env, "zignapi: failed to read arguments");
|
|
71
81
|
return null;
|
|
72
82
|
};
|
|
73
83
|
|
|
@@ -81,7 +91,7 @@ fn Wrap(comptime func: anytype) type {
|
|
|
81
91
|
var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined;
|
|
82
92
|
inline for (fn_info.params, 0..) |param, i| {
|
|
83
93
|
args[i] = convert.fromJs(param.type.?, env, argv[i], allocator) catch {
|
|
84
|
-
napi.throwError(env, "
|
|
94
|
+
napi.throwError(env, "zignapi: failed to convert argument");
|
|
85
95
|
return null;
|
|
86
96
|
};
|
|
87
97
|
}
|
|
@@ -108,7 +118,7 @@ fn Wrap(comptime func: anytype) type {
|
|
|
108
118
|
|
|
109
119
|
fn toJs(env: napi.Env, comptime T: type, value: T) napi.Value {
|
|
110
120
|
return convert.toJs(T, env, value) catch {
|
|
111
|
-
napi.throwError(env, "
|
|
121
|
+
napi.throwError(env, "zignapi: failed to convert return value");
|
|
112
122
|
return null;
|
|
113
123
|
};
|
|
114
124
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! zignapi — write native Node.js addons in Zig.
|
|
2
2
|
//!
|
|
3
3
|
//! This is the public entry point of the Zig module consumers import with
|
|
4
|
-
//! `@import("
|
|
4
|
+
//! `@import("zignapi")`. It re-exports the pieces an addon author needs:
|
|
5
5
|
//!
|
|
6
6
|
//! - `register` : expose Zig functions to JS and emit `napi_register_module_v1`.
|
|
7
7
|
//! - `napi` : the raw N-API bindings and thin wrappers, for escape hatches.
|
|
@@ -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.
|
|
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",
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"node-api",
|
|
9
9
|
"native",
|
|
10
10
|
"addon",
|
|
11
|
-
"
|
|
11
|
+
"zignapi"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://github.com/
|
|
13
|
+
"homepage": "https://github.com/zignapi/zignapi",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/
|
|
17
|
-
"directory": "packages/
|
|
16
|
+
"url": "git+https://github.com/zignapi/zignapi.git",
|
|
17
|
+
"directory": "packages/zignapi"
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
20
20
|
"bin": {
|
package/templates/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# __NAME__
|
|
2
2
|
|
|
3
3
|
A native Node.js addon written in [Zig](https://ziglang.org/) with
|
|
4
|
-
[
|
|
4
|
+
[zignapi](https://github.com/) — requires **Zig 0.16.0** and **Node >= 18**.
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
7
|
zignapi build # compiles src/main.zig into __NAME__.node (and zig-out/lib/)
|
|
8
8
|
node --test # runs test.js
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Edit `src/main.zig` to add functions, then list them in the `
|
|
11
|
+
Edit `src/main.zig` to add functions, then list them in the `zignapi.register`
|
|
12
12
|
call at the bottom of that file.
|
|
13
13
|
|
|
14
|
-
## The
|
|
14
|
+
## The zignapi dependency
|
|
15
15
|
|
|
16
16
|
`zignapi new` added the `zignapi` Zig module to `build.zig.zon` with
|
|
17
17
|
`zig fetch --save`, which pins it by content hash:
|
package/templates/build.zig
CHANGED
|
@@ -2,13 +2,13 @@ const std = @import("std");
|
|
|
2
2
|
|
|
3
3
|
/// Builds the addon into `zig-out/lib/__NAME__.node`.
|
|
4
4
|
///
|
|
5
|
-
/// The addon is a dynamic library importing the `
|
|
5
|
+
/// The addon is a dynamic library importing the `zignapi` module. N-API symbols
|
|
6
6
|
/// are left undefined at link time and resolved by Node when it loads the addon.
|
|
7
7
|
pub fn build(b: *std.Build) void {
|
|
8
8
|
const target = b.standardTargetOptions(.{});
|
|
9
9
|
const optimize = b.standardOptimizeOption(.{});
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const zignapi = b.dependency("zignapi", .{}).module("zignapi");
|
|
12
12
|
|
|
13
13
|
const addon = b.addLibrary(.{
|
|
14
14
|
.name = "__NAME__",
|
|
@@ -19,7 +19,7 @@ pub fn build(b: *std.Build) void {
|
|
|
19
19
|
.optimize = optimize,
|
|
20
20
|
.link_libc = true,
|
|
21
21
|
.imports = &.{
|
|
22
|
-
.{ .name = "
|
|
22
|
+
.{ .name = "zignapi", .module = zignapi },
|
|
23
23
|
},
|
|
24
24
|
}),
|
|
25
25
|
});
|
package/templates/build.zig.zon
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
.name = .__NAME__,
|
|
3
3
|
.version = "0.1.0",
|
|
4
4
|
.minimum_zig_version = "0.16.0",
|
|
5
|
-
// `zignapi new` fills in `.fingerprint` and adds the `
|
|
6
|
-
// below via `zig fetch --save` (which pins
|
|
5
|
+
// `zignapi new` fills in `.fingerprint` and adds the `zignapi` dependency
|
|
6
|
+
// below via `zig fetch --save` (which pins zignapi by content hash).
|
|
7
7
|
.dependencies = .{},
|
|
8
8
|
.paths = .{
|
|
9
9
|
"build.zig",
|
package/templates/dot-gitignore
CHANGED
package/templates/package.json
CHANGED
package/templates/src/main.zig
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const zignapi = @import("zignapi");
|
|
2
2
|
|
|
3
3
|
/// Exposed to JS as `addon.add(a, b)`.
|
|
4
4
|
pub fn add(a: i32, b: i32) i32 {
|
|
@@ -14,7 +14,7 @@ pub fn greet(name: []const u8) []const u8 {
|
|
|
14
14
|
// Emits `napi_register_module_v1`. Each field becomes a property on the addon's
|
|
15
15
|
// `exports`. Add your own Zig functions here.
|
|
16
16
|
comptime {
|
|
17
|
-
|
|
17
|
+
zignapi.register(.{
|
|
18
18
|
.add = add,
|
|
19
19
|
.greet = greet,
|
|
20
20
|
});
|
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
|
-
|
|
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
|
+
});
|