|
| 1 | +--- |
| 2 | +description: Use @napi-rs/cli programmatic APIs to customize your build workflow. |
| 3 | +--- |
| 4 | + |
| 5 | +# Programmatic API |
| 6 | + |
| 7 | +import { Green, Rust, Warning } from '../../../components/chalk' |
| 8 | +import { Callout } from 'nextra-theme-docs' |
| 9 | + |
| 10 | +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: |
| 11 | + |
| 12 | +- Post-process build outputs (format, transform, or validate generated files) |
| 13 | +- Integrate with custom build systems like Bazel |
| 14 | +- Generate TypeScript definitions separately from the Rust compilation |
| 15 | +- Build automation scripts with full control over the build process |
| 16 | + |
| 17 | +## Post-Processing Build Outputs |
| 18 | + |
| 19 | +The most common use case is running custom post-processing on the generated JavaScript and TypeScript files. Here's an example using <Green>oxfmt</Green> to format the output files: |
| 20 | + |
| 21 | +```ts filename="build.ts" |
| 22 | +import { readFile, writeFile } from 'node:fs/promises' |
| 23 | + |
| 24 | +import { NapiCli, createBuildCommand } from '@napi-rs/cli' |
| 25 | +import { format, type FormatOptions } from 'oxfmt' |
| 26 | + |
| 27 | +import oxfmtConfig from './.oxfmtrc.json' with { type: 'json' } |
| 28 | + |
| 29 | +const buildCommand = createBuildCommand(process.argv.slice(2)) |
| 30 | +const cli = new NapiCli() |
| 31 | +const buildOptions = buildCommand.getOptions() |
| 32 | +const { task } = await cli.build(buildOptions) |
| 33 | +const outputs = await task |
| 34 | + |
| 35 | +for (const output of outputs) { |
| 36 | + if (output.kind !== 'node') { |
| 37 | + const { code } = await format(output.path, await readFile(output.path, 'utf-8'), oxfmtConfig as FormatOptions) |
| 38 | + await writeFile(output.path, code) |
| 39 | + } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +Run this script with the same arguments you would pass to <Green>napi build</Green>: |
| 44 | + |
| 45 | +```sh |
| 46 | +node ./build.ts --release --platform |
| 47 | +``` |
| 48 | + |
| 49 | +### How It Works |
| 50 | + |
| 51 | +1. `createBuildCommand(args)` parses CLI arguments and returns a `BuildCommand` instance |
| 52 | +2. `buildCommand.getOptions()` extracts the parsed options as a plain object |
| 53 | +3. `cli.build(options)` starts the build and returns `{ task, abort }` |
| 54 | +4. `await task` waits for completion and returns an array of `Output` objects |
| 55 | + |
| 56 | +### Output Types |
| 57 | + |
| 58 | +Each item in the outputs array has this structure: |
| 59 | + |
| 60 | +```ts |
| 61 | +type OutputKind = 'js' | 'dts' | 'node' | 'exe' | 'wasm' |
| 62 | + |
| 63 | +type Output = { |
| 64 | + kind: OutputKind |
| 65 | + path: string // Absolute path to the output file |
| 66 | +} |
| 67 | +``` |
| 68 | +
|
| 69 | +| Kind | Description | |
| 70 | +|------|-------------| |
| 71 | +| <Green>node</Green> | Native Node.js addon (<Green>.node</Green> file) | |
| 72 | +| <Green>js</Green> | JavaScript binding file | |
| 73 | +| <Green>dts</Green> | TypeScript definition file | |
| 74 | +| <Green>exe</Green> | Executable binary | |
| 75 | +| <Green>wasm</Green> | WebAssembly module | |
| 76 | +
|
| 77 | +## Standalone Types/JS Generation |
| 78 | +
|
| 79 | +<Callout type="info"> |
| 80 | +This is useful for build systems like Bazel that handle Rust compilation separately and only need the TypeScript type generation step. |
| 81 | +</Callout> |
| 82 | +
|
| 83 | +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: |
| 84 | +
|
| 85 | +```ts filename="generate-types.ts" |
| 86 | +import { spawn } from 'node:child_process' |
| 87 | +import { mkdir, writeFile, copyFile, rm } from 'node:fs/promises' |
| 88 | +import { join, dirname } from 'node:path' |
| 89 | +import { fileURLToPath } from 'node:url' |
| 90 | + |
| 91 | +import { generateTypeDef, writeJsBinding, parseTriple } from '@napi-rs/cli' |
| 92 | + |
| 93 | +import pkg from './package.json' with { type: 'json' } |
| 94 | + |
| 95 | +const currentTarget = 'x86_64-unknown-linux-gnu' |
| 96 | + |
| 97 | +const currentDir = dirname(fileURLToPath(import.meta.url)) |
| 98 | +const typeDefDir = join(currentDir, 'target', 'napi-rs', 'YOUR_PKG_NAME') |
| 99 | +const triple = parseTriple(currentTarget) |
| 100 | +const bindingName = `customized.${triple.platformArchABI}.node` |
| 101 | + |
| 102 | +await mkdir(typeDefDir, { recursive: true }) |
| 103 | + |
| 104 | +const childProcess = spawn('cargo', ['build', '--release'], { |
| 105 | + stdio: 'pipe', |
| 106 | + env: { |
| 107 | + NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir, |
| 108 | + ...process.env, |
| 109 | + }, |
| 110 | +}) |
| 111 | + |
| 112 | +// Remove old binding file, this is necessary on some platforms like macOS |
| 113 | +// copy the new binding file without removing the old one will cause weird segmentation fault |
| 114 | +await rm(join(currentDir, bindingName)).catch(() => { |
| 115 | + // ignore error |
| 116 | +}) |
| 117 | + |
| 118 | +await copyFile(join(currentDir, 'target', currentTarget, 'release', 'libfoo.so'), join(currentDir, bindingName)) |
| 119 | + |
| 120 | +childProcess.stdout.on('data', (data) => { |
| 121 | + console.log(data.toString()) |
| 122 | +}) |
| 123 | + |
| 124 | +childProcess.stderr.on('data', (data) => { |
| 125 | + console.error(data.toString()) |
| 126 | +}) |
| 127 | + |
| 128 | +await new Promise((resolve, reject) => { |
| 129 | + childProcess.on('error', (error) => { |
| 130 | + reject(error) |
| 131 | + }) |
| 132 | + childProcess.on('close', (code) => { |
| 133 | + if (code === 0) { |
| 134 | + resolve(true) |
| 135 | + } else { |
| 136 | + reject(new Error(`cargo build --release failed with code ${code}`)) |
| 137 | + } |
| 138 | + }) |
| 139 | +}) |
| 140 | + |
| 141 | +const { dts, exports } = await generateTypeDef({ |
| 142 | + typeDefDir, |
| 143 | + cwd: process.cwd(), |
| 144 | +}) |
| 145 | + |
| 146 | +await writeFile(join(currentDir, 'customized.d.ts'), dts) |
| 147 | + |
| 148 | +await writeJsBinding({ |
| 149 | + jsBinding: 'customized.js', |
| 150 | + platform: true, |
| 151 | + binaryName: pkg.napi.binaryName, |
| 152 | + packageName: pkg.name, |
| 153 | + version: pkg.version, |
| 154 | + outputDir: currentDir, |
| 155 | + idents: exports |
| 156 | +}) |
| 157 | +``` |
| 158 | + |
| 159 | +<Callout type="warning"> |
| 160 | +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`. |
| 161 | +</Callout> |
| 162 | + |
| 163 | +### Control Flow |
| 164 | + |
| 165 | +``` |
| 166 | +┌─────────────────────────────────────────────────────────────────────────┐ |
| 167 | +│ SETUP PHASE │ |
| 168 | +├─────────────────────────────────────────────────────────────────────────┤ |
| 169 | +│ 1. Read package.json for napi config │ |
| 170 | +│ 2. Get target triple (from cli flag) (e.g.,'x86_64-unknown-linux-gnu') │ |
| 171 | +│ 3. parseTriple() → get platformArchABI for binding filename │ |
| 172 | +│ 4. mkdir(typeDefDir) → create directory for type definitions │ |
| 173 | +└─────────────────────────────────────────────────────────────────────────┘ |
| 174 | + │ |
| 175 | + ▼ |
| 176 | +┌─────────────────────────────────────────────────────────────────────────┐ |
| 177 | +│ BUILD PHASE │ |
| 178 | +├─────────────────────────────────────────────────────────────────────────┤ |
| 179 | +│ 5. spawn('cargo', ['build', '--release']) │ |
| 180 | +│ └─ env: { NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir } │ |
| 181 | +│ ▲ │ |
| 182 | +│ └─ This env var tells napi-derive where to write type defs │ |
| 183 | +│ │ |
| 184 | +│ 6. rm(old binding) → Remove old .node file (prevents macOS segfault) │ |
| 185 | +│ 7. copyFile(libfoo.so → customized.{platform}.node) │ |
| 186 | +│ 8. Stream stdout/stderr from cargo │ |
| 187 | +│ 9. await cargo completion │ |
| 188 | +└─────────────────────────────────────────────────────────────────────────┘ |
| 189 | + │ |
| 190 | + ▼ |
| 191 | +┌─────────────────────────────────────────────────────────────────────────┐ |
| 192 | +│ TYPE GENERATION PHASE │ |
| 193 | +├─────────────────────────────────────────────────────────────────────────┤ |
| 194 | +│ 10. generateTypeDef({ typeDefDir, cwd }) │ |
| 195 | +│ └─ Reads intermediate .json files from typeDefDir │ |
| 196 | +│ └─ Returns { dts: string, exports: string[] } │ |
| 197 | +│ │ |
| 198 | +│ 11. writeFile('customized.d.ts', dts) │ |
| 199 | +│ │ |
| 200 | +│ 12. writeJsBinding({ platform, binaryName, idents: exports, ... }) │ |
| 201 | +│ └─ Generates JS loader that imports the .node file │ |
| 202 | +└─────────────────────────────────────────────────────────────────────────┘ |
| 203 | + │ |
| 204 | + ▼ |
| 205 | + ┌───────────┐ |
| 206 | + │ DONE │ |
| 207 | + │ │ |
| 208 | + │ Output: │ |
| 209 | + │ • .node │ |
| 210 | + │ • .d.ts │ |
| 211 | + │ • .js │ |
| 212 | + └───────────┘ |
| 213 | +``` |
| 214 | + |
| 215 | +### Key Concepts |
| 216 | + |
| 217 | +#### The `NAPI_TYPE_DEF_TMP_FOLDER` Environment Variable |
| 218 | + |
| 219 | +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: |
| 220 | + |
| 221 | +``` |
| 222 | +Rust Code → napi-derive macro → JSON files → generateTypeDef() → .d.ts |
| 223 | +``` |
| 224 | + |
| 225 | +#### Platform-Specific Binding Names |
| 226 | + |
| 227 | +The `parseTriple()` function extracts platform information from a target triple: |
| 228 | + |
| 229 | +```ts |
| 230 | +const triple = parseTriple('x86_64-unknown-linux-gnu') |
| 231 | +// Returns: { platform: 'linux', arch: 'x64', abi: 'gnu', platformArchABI: 'linux-x64-gnu', ... } |
| 232 | + |
| 233 | +const bindingName = `mylib.${triple.platformArchABI}.node` |
| 234 | +// Result: 'mylib.linux-x64-gnu.node' |
| 235 | +``` |
| 236 | + |
| 237 | +#### Removing Old Binding Files |
| 238 | + |
| 239 | +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: |
| 240 | + |
| 241 | +```ts |
| 242 | +await rm(join(currentDir, bindingName)).catch(() => { |
| 243 | + // ignore error if file doesn't exist |
| 244 | +}) |
| 245 | +await copyFile(sourceLib, join(currentDir, bindingName)) |
| 246 | +``` |
| 247 | + |
| 248 | +### GenerateTypeDefOptions |
| 249 | + |
| 250 | +| Option | Type | Required | Default | Description | |
| 251 | +|--------|------|----------|---------|-------------| |
| 252 | +| typeDefDir | string | Yes | | Directory containing intermediate type def files | |
| 253 | +| cwd | string | Yes | | Working directory for resolving relative paths | |
| 254 | +| noDtsHeader | boolean | No | false | Skip the default file header | |
| 255 | +| dtsHeader | string | No | | Custom header string for the <Green>.d.ts</Green> file | |
| 256 | +| dtsHeaderFile | string | No | | Path to a file containing the header content | |
| 257 | +| configDtsHeader | string | No | | Header from config (lower priority than dtsHeader) | |
| 258 | +| configDtsHeaderFile | string | No | | Header file from config (highest priority) | |
| 259 | +| constEnum | boolean | No | true | Generate <Green>const enum</Green> instead of regular enum | |
| 260 | + |
| 261 | +### WriteJsBindingOptions |
| 262 | + |
| 263 | +| Option | Type | Required | Default | Description | |
| 264 | +|--------|------|----------|---------|-------------| |
| 265 | +| platform | boolean | No | false | Required to generate JS binding; adds platform triple | |
| 266 | +| noJsBinding | boolean | No | false | Skip JS binding generation | |
| 267 | +| idents | string[] | Yes | | Exported identifiers from <Green>generateTypeDef</Green> | |
| 268 | +| jsBinding | string | No | 'index.js' | Custom filename for the JS binding | |
| 269 | +| esm | boolean | No | false | Generate ESM format instead of CommonJS | |
| 270 | +| binaryName | string | Yes | | Name of the native binary | |
| 271 | +| packageName | string | Yes | | Package name for require/import statements | |
| 272 | +| version | string | Yes | | Package version | |
| 273 | +| outputDir | string | Yes | | Directory to write the JS binding file | |
| 274 | + |
| 275 | +## Other Exported APIs |
| 276 | + |
| 277 | +### NapiCli Class |
| 278 | + |
| 279 | +The main class for programmatic access to all CLI commands: |
| 280 | + |
| 281 | +```ts |
| 282 | +import { NapiCli } from '@napi-rs/cli' |
| 283 | + |
| 284 | +const cli = new NapiCli() |
| 285 | + |
| 286 | +// Available methods: |
| 287 | +cli.build(options) // Build the project |
| 288 | +cli.artifacts(options) // Collect artifacts from CI |
| 289 | +cli.new(options) // Create new project |
| 290 | +cli.createNpmDirs(options)// Create npm package directories |
| 291 | +cli.prePublish(options) // Prepare for publishing |
| 292 | +cli.rename(options) // Rename project |
| 293 | +cli.universalize(options) // Create universal binaries |
| 294 | +cli.version(options) // Update versions |
| 295 | +``` |
| 296 | + |
| 297 | +### Command Creators |
| 298 | + |
| 299 | +Parse CLI arguments into command option objects: |
| 300 | + |
| 301 | +```ts |
| 302 | +import { |
| 303 | + createBuildCommand, |
| 304 | + createArtifactsCommand, |
| 305 | + createCreateNpmDirsCommand, |
| 306 | + createPrePublishCommand, |
| 307 | + createRenameCommand, |
| 308 | + createUniversalizeCommand, |
| 309 | + createVersionCommand, |
| 310 | + createNewCommand, |
| 311 | +} from '@napi-rs/cli' |
| 312 | + |
| 313 | +// Parse arguments as if running `napi build --release --platform` |
| 314 | +const buildCmd = createBuildCommand(['--release', '--platform']) |
| 315 | +const options = buildCmd.getOptions() |
| 316 | +``` |
| 317 | + |
| 318 | +### Utility Functions |
| 319 | + |
| 320 | +```ts |
| 321 | +import { parseTriple, readNapiConfig } from '@napi-rs/cli' |
| 322 | + |
| 323 | +// Parse target triple string |
| 324 | +const triple = parseTriple('x86_64-unknown-linux-gnu') |
| 325 | +// { platform: 'linux', arch: 'x64', abi: 'gnu', ... } |
| 326 | + |
| 327 | +// Read napi config from package.json or napi.json |
| 328 | +const config = await readNapiConfig('/path/to/project') |
| 329 | +``` |
| 330 | + |
| 331 | +## Aborting a Build |
| 332 | + |
| 333 | +The `build()` method returns an `abort` function to cancel the build: |
| 334 | + |
| 335 | +```ts |
| 336 | +const { task, abort } = await cli.build(options) |
| 337 | + |
| 338 | +// Handle SIGINT to abort cleanly |
| 339 | +process.on('SIGINT', () => { |
| 340 | + abort() |
| 341 | + process.exit(1) |
| 342 | +}) |
| 343 | + |
| 344 | +const outputs = await task |
| 345 | +``` |
0 commit comments