Project layout

A @teakit/napi project is a Zig project and a Node.js project sharing one root. This page is a one-stop map: every file in the project, every directory the build creates, and where to look for more on each.

The same layout applies whether you scaffolded with napikit new or wired things up by hand following Manual setup.

Source tree

my-addon/
├── build.zig              # napi.addLib entry point
├── build.zig.zon          # Zig dependency lockfile
├── package.json           # npm name, version, scripts
├── src/
│   └── lib.zig            # your addon's root module
├── test.mjs               # smoke test
├── .gitignore
├── README.md
└── .github/
    └── workflows/
        └── publish.yml    # tag-driven publish (OIDC, no NPM_TOKEN)
File / dirPurpose
build.zigThe single call to napi.addLib configures the artifact and the npm release graph.
build.zig.zonPins the napi Zig dependency. Zig manages this; you rarely edit it.
package.jsonThe local Node project. Holds the exact package-manager baseline, dev deps, and napikit build / napikit bump script aliases.
src/lib.zigThe root module. Everything pub in this file is a JS export. See Functions. To use a different file or location, update .root in build.zig.
test.mjsLoads the built addon and asserts the starter behavior. npm, pnpm, and Yarn projects run it with Node; Bun projects use bun run test.mjs.
.github/workflows/publish.ymlTag-driven CI publish. See Publishing.

After napikit build

zig-out/
├── lib/
│   ├── <name>.node        # the native module Node loads
│   └── <name>.d.ts        # generated TypeScript types (if .dts is set)
└── napikit/loaders/
    ├── <name>.cjs         # build-owned CommonJS loader source
    └── <name>.mjs         # build-owned ESM loader source

<name>.js                  # CLI-written re-exporter at the project root
<name>.d.ts                # copied alongside <name>.js (if .dts is set)

<name>.js lets both import addon, { namedExport } from "./<name>.js" and the matching CommonJS load work locally without going through the npm package layout. It points at zig-out/lib/<name>.node and follows the root package.json module mode: "type": "module" selects ESM, while "type": "commonjs" or an omitted type selects CommonJS. Zig writes export-aware wrapper sources under zig-out/napikit/loaders/, and the CLI copies the appropriate one to the root.

Both <name>.js and <name>.d.ts at the project root are gitignored derived artifacts. Generated loaders and root declaration copies carry an ownership header; changing package.json.type migrates a loader that still has that header (and the legacy canonical wrapper), while arbitrary user-edited loaders and declarations are preserved. A legacy headerless declaration copy is recognized by matching it to the previous installed declaration, then migrated to the owned form. The CLI verifies that an observed generated/absent loader has not changed before its atomic replacement, so a concurrent user edit is never silently overwritten. Invalid package.json data fails before Zig or output cleanup. If declaration generation is later disabled, the next successful napikit build removes only the stale generated <name>.d.ts files from zig-out/lib/ and the project root.

Before invoking Zig, the CLI snapshots and clears prior native addons and their adjacent declarations. A successful run must produce at least one regular .node; a failed or empty run restores the last usable native outputs, declarations, and wrapper sidecars. This also makes addon renames authoritative: retired build-owned root loaders and declarations are removed after success, while same-named user files remain untouched.

Before each Zig invocation, the CLI clears previous build-owned wrapper sidecars and restores them only if the invocation fails. This ensures a successful build consumes metadata from that same invocation rather than stale export information.

If a newer napikit is paired with an older pinned @teakit/napi Zig dependency that does not emit export-aware wrapper sidecars, the CLI writes a default-export-only compatibility loader and tells you to update the Zig dependency. Default imports continue to work; named imports become available again after the pins are synchronized. A custom root loader is preserved even when those sidecars are absent.

After napikit build --release

npm/<name>/
├── package.json
├── index.js               # your seam over the addon
├── binding.js             # platform detection + dynamic require
├── index.d.ts             # types (if .dts is set)
└── <scope>/
    └── binding-<os>-<arch>[-<libc>]/
        ├── package.json
        └── <name>.node

This is the publishable tree. napikit publish ships every package discovered under npm/ to npm. The tree is reconciled on every release build: policy fields come from build.zig, your version and user-edited fields are preserved. With .dts = .none, index.d.ts and the manifest's types/managed files entries are absent; switching to .none removes that stale build-owned declaration. See Cross-compiling for the full rules.

If build.zig calls addLib more than once, each addon gets its own npm/<name>/ subtree alongside the others. See Multiple addons in one repo.

Where to put your code

  • One file is fine. Keep adding to src/lib.zig until it feels too large.
  • Splitting into namespaces. A pub const x = @import("x.zig") in lib.zig becomes the JS namespace addon.x. See Namespaces.
  • Shared Zig modules outside the root. Pass them through .imports so they are importable as @import("name") from anywhere in your addon. See addLib Import.

Generated and ignored

Exclude everything derived from version control: node_modules, zig-out, .zig-cache, zig-pkg, and the project-root <name>.js / <name>.d.ts re-exporters. (napikit new writes a .gitignore with all of these; if you set up by hand, see Manual setup, step 5.) Commit npm/ once you start publishing. Every release build keeps it in sync with build.zig automatically.