How I Published My Rust Bun Version Manager (bum) CLI to NPM Package
Source: Dev.to
Background
I built a CLI called bum – a fast Bun version manager written in Rust. It works great locally, but I wanted anyone to be able to run:
npx @owenizedd/bum use 1.3.3
without installing Rust or compiling anything. Turns out this is totally possible! I learned about it from an article by lekoarts.de about publishing Rust CLIs on npm using napi‑rs.
The Setup
Add napi-rs to your Rust project
# Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
napi = "2.16"
napi-derive = "2.16"
[build-dependencies]
napi-build = "2.1"
Wrap your CLI in a napi function
// src/lib.rs
use napi_derive::napi;
#[napi]
pub fn run(args: Vec) -> napi::Result {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| napi::Error::from_reason(format!("Failed to create runtime: {}", e)))?;
// Your CLI logic here
rt.block_on(async {
// run_commands(args).await
});
Ok(())
}
Create a simple bin.js
#!/usr/bin/env node
const { run } = require("./index");
const args = process.argv.slice(2);
try {
run(args);
} catch (e) {
console.error(e);
process.exit(1);
}
Configure package.json
{
"name": "@owenizedd/bum",
"bin": "bin.js",
"napi": {
"name": "bum",
"triples": {
"defaults": true,
"additional": [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu"
]
}
}
}
OpenSSL Cross‑Compilation Hell
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
zip = { version = "1.1", default-features = false, features = ["deflate"] }
Each platform (e.g., darwin-arm64, linux-x64-gnu) is published as a separate npm package. Initially we forgot to update their package.json versions, so npm couldn’t find them.
Fix: Sync platform package versions in CI
- name: Sync platform package versions
run: |
VERSION=$(node -p "require('./package.json').version")
for dir in npm/*/; do
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('$dir/package.json'));
pkg.version = '$VERSION';
fs.writeFileSync('$dir/package.json', JSON.stringify(pkg, null, 2));
"
done
Bundling Pitfalls
We initially tried to compile TypeScript with bun build bin.ts. Bun inlined require('./index') and hard‑coded the .node filename with a hash, producing broken code:
// Broken bundling result
nativeBinding = require("./bum.darwin-arm64-7xvffnqw.node");
The correct (working) import should be:
// Correct import
nativeBinding = require("./bum.darwin-arm64.node");
Solution: Use plain JavaScript for bin.js and avoid bundling.
Result
Now anyone can run:
npx @owenizedd/bum use 1.3.3
and it works on macOS, Linux, and Windows (still being tested on other platforms).
References
- Publishing a Rust CLI on npm – the article that made this possible
- napi‑rs Documentation
- bum GitHub Repository