Skip to content

SOP: Build, Test & Publish to npm

Fresh 🌱

Goal: Move a Neon module from local development to a published npm package, including the cross-platform concern that makes native modules harder to ship than pure-JS packages.

Done when: You have a release binary that works, and a clear plan for distributing it.

Step 1 - The build loop

Two build modes, used at different times:

shell
# Debug build - fast to compile, slower to run. Use while iterating.
npm run build

# Release build - slow to compile, fast to run. Use to measure or ship.
npm run build -- --release

Both produce index.node at the project root. That single binary is what require('.') loads.

flowchart LR
    SRC[src/lib.rs] -->|npm run build| CARGO[cargo compiles crate]
    CARGO --> NODE[index.node]
    NODE -->|require '.'| JS[Your JS / Node app]
    style NODE fill:#10b981,color:#fff

Step 2 - Test from Node

The fastest feedback loop is the Node REPL at the project root:

shell
node
> const addon = require('.')
> addon.get()

For automated tests, write a normal JS test (any runner) that require('.') and asserts on the exported functions. Because the module is just an npm package, your usual npm test flow applies.

Keep a JS wrapper

It is common to keep a thin index.js (or lib/index.js) that require('./index.node') and re-exports a clean, idiomatic JavaScript API on top of the raw native exports. JavaScript consumers import the wrapper, not the binary.

Step 3 - The cross-platform problem

A native module compiles to a binary for one specific platform and architecture (for example, macOS arm64, Windows x64, Linux x64). A user on a different platform cannot run your binary. There are two common strategies:

StrategyHow it worksTrade-off
Build on installShip source; compile in the user's postinstall.Every consumer needs the full Rust toolchain installed. Slow installs.
PrebuildsBuild binaries for each target ahead of time, publish them, and download the matching one at install.More release machinery, but consumers need no Rust toolchain. This is the preferred approach for libraries.

For a published library, prebuilds are the standard: you produce one binary per supported platform/arch in CI, publish them, and the package resolves the correct binary at install time so end users never compile anything.

Step 4 - Prepare package.json for publishing

Treat it like any npm package, with attention to what gets published:

  • Set name, version, description, license, and repository.
  • Use the files field (or .npmignore) so you do not publish the heavy target/ directory.
  • Decide whether index.node (or per-platform prebuild packages) is included in the published artifact.
  • Point main at your JS wrapper if you have one.

Step 5 - Publish

shell
npm publish

For prebuild-based distribution, the publish step is driven by your release pipeline (CI builds each platform, then publishes the main package plus the per-platform binary packages).

Checklist

  • [ ] npm run build -- --release produces a working index.node
  • [ ] Module verified from the Node REPL and/or npm test
  • [ ] A distribution strategy chosen (build-on-install vs prebuilds)
  • [ ] package.json excludes target/ and includes the right artifacts
  • [ ] npm publish run (directly, or via the release pipeline)

Do not ship target/

The Rust target/ directory can be hundreds of megabytes. Always exclude it from the published package via files or .npmignore.

Where the heavy lifting lives

Most of the publishing complexity for native modules is the prebuild matrix (build once per platform/arch in CI). The Neon project provides tooling and project conventions to make this manageable, but the concept is universal to native Node addons.