Appearance
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 -- --releaseBoth 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:#fffStep 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:
| Strategy | How it works | Trade-off |
|---|---|---|
| Build on install | Ship source; compile in the user's postinstall. | Every consumer needs the full Rust toolchain installed. Slow installs. |
| Prebuilds | Build 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, andrepository. - Use the
filesfield (or.npmignore) so you do not publish the heavytarget/directory. - Decide whether
index.node(or per-platform prebuild packages) is included in the published artifact. - Point
mainat your JS wrapper if you have one.
Step 5 - Publish
shell
npm publishFor 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 -- --releaseproduces a workingindex.node - [ ] Module verified from the Node REPL and/or
npm test - [ ] A distribution strategy chosen (build-on-install vs prebuilds)
- [ ]
package.jsonexcludestarget/and includes the right artifacts - [ ]
npm publishrun (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.