Programmatic API
The @napi-rs/cli package exports programmatic APIs that allow you to customize your build workflow beyond what the CLI commands offer. This is useful when you need to:
- Post-process build outputs (format, transform, or validate generated files)
- Integrate with custom build systems like Bazel
- Generate TypeScript definitions separately from the Rust compilation
- Build automation scripts with full control over the build process
Post-Processing Build Outputs
The most common use case is running custom post-processing on the generated JavaScript and TypeScript files. Here's an example using oxfmt to format the output files:
import { readFile, writeFile } from 'node:fs/promises'
import { NapiCli, createBuildCommand } from '@napi-rs/cli'
import { format, type FormatOptions } from 'oxfmt'
import oxfmtConfig from './.oxfmtrc.json' with { type: 'json' }
const buildCommand = createBuildCommand(process.argv.slice(2))
const cli = new NapiCli()
const buildOptions = buildCommand.getOptions()
const { task } = await cli.build(buildOptions)
const outputs = await task
for (const output of outputs) {
if (output.kind !== 'node') {
const { code } = await format(output.path, await readFile(output.path, 'utf-8'), oxfmtConfig as FormatOptions)
await writeFile(output.path, code)
}
}Run this script with the same arguments you would pass to napi build:
node ./build.ts --release --platformHow It Works
createBuildCommand(args)parses CLI arguments and returns aBuildCommandinstancebuildCommand.getOptions()extracts the parsed options as a plain objectcli.build(options)starts the build and returns{ task, abort }await taskwaits for completion and returns an array ofOutputobjects
Output Types
Each item in the outputs array has this structure:
type OutputKind = 'js' | 'dts' | 'node' | 'exe' | 'wasm'
type Output = {
kind: OutputKind
path: string // Absolute path to the output file
}| Kind | Description |
|---|---|
| node | Native Node.js addon (.node file) |
| js | JavaScript binding file |
| dts | TypeScript definition file |
| exe | Executable binary |
| wasm | WebAssembly module |
Standalone Types/JS Generation
This is useful for build systems like Bazel that handle Rust compilation separately and only need the TypeScript type generation step.
If you compile Rust code outside of @napi-rs/cli (e.g., using Bazel's rust_shared_library), you can still generate TypeScript definitions using the generateTypeDef and writeJsBinding APIs:
import { spawn } from 'node:child_process'
import { mkdir, writeFile, copyFile, rm } from 'node:fs/promises'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { generateTypeDef, writeJsBinding, parseTriple } from '@napi-rs/cli'
import pkg from './package.json' with { type: 'json' }
const currentTarget = 'x86_64-unknown-linux-gnu'
const currentDir = dirname(fileURLToPath(import.meta.url))
const typeDefDir = join(currentDir, 'target', 'napi-rs', 'YOUR_PKG_NAME')
const triple = parseTriple(currentTarget)
const bindingName = `customized.${triple.platformArchABI}.node`
await mkdir(typeDefDir, { recursive: true })
const childProcess = spawn('cargo', ['build', '--release'], {
stdio: 'pipe',
env: {
NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir,
...process.env,
},
})
// Remove old binding file, this is necessary on some platforms like macOS
// copy the new binding file without removing the old one will cause weird segmentation fault
await rm(join(currentDir, bindingName)).catch(() => {
// ignore error
})
await copyFile(join(currentDir, 'target', currentTarget, 'release', 'libfoo.so'), join(currentDir, bindingName))
childProcess.stdout.on('data', (data) => {
console.log(data.toString())
})
childProcess.stderr.on('data', (data) => {
console.error(data.toString())
})
await new Promise((resolve, reject) => {
childProcess.on('error', (error) => {
reject(error)
})
childProcess.on('close', (code) => {
if (code === 0) {
resolve(true)
} else {
reject(new Error(`cargo build --release failed with code ${code}`))
}
})
})
const { dts, exports } = await generateTypeDef({
typeDefDir,
cwd: process.cwd(),
})
await writeFile(join(currentDir, 'customized.d.ts'), dts)
await writeJsBinding({
jsBinding: 'customized.js',
platform: true,
binaryName: pkg.napi.binaryName,
packageName: pkg.name,
version: pkg.version,
outputDir: currentDir,
idents: exports
})The typeDefDir must contain the intermediate type definition files generated by the napi-derive proc macro when the type-def feature is enabled. These files are normally created in a temporary directory during napi build.
Control Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ SETUP PHASE │
├─────────────────────────────────────────────────────────────────────────┤
│ 1. Read package.json for napi config │
│ 2. Get target triple (from cli flag) (e.g.,'x86_64-unknown-linux-gnu') │
│ 3. parseTriple() → get platformArchABI for binding filename │
│ 4. mkdir(typeDefDir) → create directory for type definitions │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ BUILD PHASE │
├─────────────────────────────────────────────────────────────────────────┤
│ 5. spawn('cargo', ['build', '--release']) │
│ └─ env: { NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir } │
│ ▲ │
│ └─ This env var tells napi-derive where to write type defs │
│ │
│ 6. rm(old binding) → Remove old .node file (prevents macOS segfault) │
│ 7. copyFile(libfoo.so → customized.{platform}.node) │
│ 8. Stream stdout/stderr from cargo │
│ 9. await cargo completion │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ TYPE GENERATION PHASE │
├─────────────────────────────────────────────────────────────────────────┤
│ 10. generateTypeDef({ typeDefDir, cwd }) │
│ └─ Reads intermediate .json files from typeDefDir │
│ └─ Returns { dts: string, exports: string[] } │
│ │
│ 11. writeFile('customized.d.ts', dts) │
│ │
│ 12. writeJsBinding({ platform, binaryName, idents: exports, ... }) │
│ └─ Generates JS loader that imports the .node file │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────┐
│ DONE │
│ │
│ Output: │
│ • .node │
│ • .d.ts │
│ • .js │
└───────────┘Key Concepts
The NAPI_TYPE_DEF_TMP_FOLDER Environment Variable
When you run cargo build with NAPI_TYPE_DEF_TMP_FOLDER set, the napi-derive proc macro writes intermediate type definition files (JSON format) to that directory. This is how type information flows from Rust to TypeScript:
Rust Code → napi-derive macro → JSON files → generateTypeDef() → .d.tsPlatform-Specific Binding Names
The parseTriple() function extracts platform information from a target triple:
const triple = parseTriple('x86_64-unknown-linux-gnu')
// Returns: { platform: 'linux', arch: 'x64', abi: 'gnu', platformArchABI: 'linux-x64-gnu', ... }
const bindingName = `mylib.${triple.platformArchABI}.node`
// Result: 'mylib.linux-x64-gnu.node'Removing Old Binding Files
On macOS/Linux, copying a new .node file over an existing one without first removing it can cause segmentation faults. Always remove the old file first:
await rm(join(currentDir, bindingName)).catch(() => {
// ignore error if file doesn't exist
})
await copyFile(sourceLib, join(currentDir, bindingName))GenerateTypeDefOptions
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| typeDefDir | string | Yes | Directory containing intermediate type def files | |
| cwd | string | Yes | Working directory for resolving relative paths | |
| noDtsHeader | boolean | No | false | Skip the default file header |
| dtsHeader | string | No | Custom header string for the .d.ts file | |
| dtsHeaderFile | string | No | Path to a file containing the header content | |
| configDtsHeader | string | No | Header from config (lower priority than dtsHeader) | |
| configDtsHeaderFile | string | No | Header file from config (highest priority) | |
| constEnum | boolean | No | true | Generate const enum instead of regular enum |
WriteJsBindingOptions
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| platform | boolean | No | false | Required to generate JS binding; adds platform triple |
| noJsBinding | boolean | No | false | Skip JS binding generation |
| idents | string[] | Yes | Exported identifiers from generateTypeDef | |
| jsBinding | string | No | 'index.js' | Custom filename for the JS binding |
| esm | boolean | No | false | Generate ESM format instead of CommonJS |
| binaryName | string | Yes | Name of the native binary | |
| packageName | string | Yes | Package name for require/import statements | |
| version | string | Yes | Package version | |
| outputDir | string | Yes | Directory to write the JS binding file |
Other Exported APIs
NapiCli Class
The main class for programmatic access to all CLI commands:
import { NapiCli } from '@napi-rs/cli'
const cli = new NapiCli()
// Available methods:
cli.build(options) // Build the project
cli.artifacts(options) // Collect artifacts from CI
cli.new(options) // Create new project
cli.createNpmDirs(options)// Create npm package directories
cli.prePublish(options) // Prepare for publishing
cli.rename(options) // Rename project
cli.universalize(options) // Create universal binaries
cli.version(options) // Update versionsCommand Creators
Parse CLI arguments into command option objects:
import {
createBuildCommand,
createArtifactsCommand,
createCreateNpmDirsCommand,
createPrePublishCommand,
createRenameCommand,
createUniversalizeCommand,
createVersionCommand,
createNewCommand,
} from '@napi-rs/cli'
// Parse arguments as if running `napi build --release --platform`
const buildCmd = createBuildCommand(['--release', '--platform'])
const options = buildCmd.getOptions()Utility Functions
import { parseTriple, readNapiConfig } from '@napi-rs/cli'
// Parse target triple string
const triple = parseTriple('x86_64-unknown-linux-gnu')
// { platform: 'linux', arch: 'x64', abi: 'gnu', ... }
// Read napi config from package.json or napi.json
const config = await readNapiConfig('/path/to/project')Aborting a Build
The build() method returns an abort function to cancel the build:
const { task, abort } = await cli.build(options)
// Handle SIGINT to abort cleanly
process.on('SIGINT', () => {
abort()
process.exit(1)
})
const outputs = await task