How I Published My Rust Bun Version Manager (bum) CLI to NPM Package

Background

So I built this CLI called bum – a fast Bun version manager written in Rust. Works great locally, but I wanted anyone to just run:

npx @owenizedd/bum use 1.3.3 

without installing Rust or compiling anything. That’s going to be freaking awesome right?

Turns out, this is totally possible! I learned about it from this awesome article by lekoarts.de about publishing Rust CLIs on npm using napi-rs.

The Setup
The magic is napi-rs – it compiles your Rust code into native Node.js addons (.node files) for each platform.

  1. 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"
  1. Wrap your CLI in a napi function
/ src/lib.rs
use napi_derive::napi;

#[napi]
pub fn run(args: Vec<String>) -> 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(())
}
  1. 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);
}
  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"
      ]
    }
  }
}

The Challenges

OpenSSL Cross-Compilation Hell
Linux Docker builds kept failing with OpenSSL errors. The fix? Use pure Rust alternatives:

Use rustls instead of native-tls

reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }

Use pure Rust zip compression

zip = { version = "1.1", default-features = false, features = ["deflate"] }

Platform Package Versions Not Syncing

Each platform (darwin-arm64, linux-x64-gnu, etc.) is published as a separate npm package. We forgot to update their package.json versions, so npm couldn’t find them!

Fixed by adding a CI step:

- 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

Bun Bundling Breaking Everything

Initially we used bun build bin.ts to compile TypeScript. Bad idea – Bun inlined require(‘./index’) and hardcoded the .node filename with a hash!

// Bun bundled this (broken):
`nativeBinding = require("./bum.darwin-arm64-7xvffnqw.node");`

// Should be (working):
`nativeBinding = require("./bum.darwin-arm64.node");`
Solution: Just use plain JavaScript for bin.js. No bundling needed.

Now anyone can run:

npx @owenizedd/bum use 1.3.3

And it just works on macOS, Linux, and Windows (Hopefully! not fully tested yet in other platform just on my Macbook😅 )

Honestly, there might be still a lot of issues, this progress made me so happy so far. Opus 4.5 has made this possible fast as well.

Resources

Similar Posts