Write Once, Bun Anywhere: Cross-platform Scripts with bun.js

I recently wrote a script to run on my m4 mac, windows desktop, and arch linux laptop. Specifically, the script helps me install the bleeding edge pre release of a desktop application[1] I use on all my computers. My scripts are all tracked by a dotfile manager[2]. I wanted to be able to run the exact same script on each machine to smooth out the update process.

You can find the actual script itself here: update-trilium.js[3]

Requirements for a successful cross-platform script

The script must:

  1. Work on Mac, Windows, and Linux.
  2. Be a single, self-contained text file (rather than a binary).
    1. This also means no adjacent files like requirements.txt, package.json should be present.
  3. Have access to libraries and dependencies from the web (i.e. npm packages). This unlocks a lot of powerful pre-built tooling for us.
  4. Be runnable with a single command. We want to be able to type the exact same string into the shell on any platform and have it run i.e. bun run script.js.
  5. Have no build step, we should be able to run the raw script file anywhere as it appears in source control. We don't want something like build && run script.js.
  6. Have an easy to install language/framework which runs the script (i.e. bun.js). You should only have to do this once.
    1. Similarly, we want to avoid any additional system-level dependencies or package manager commands.
    2. Also, we want no docker involvement.
  7. Allow us to modify the content of the script and run it again, auto-installing any newly introduced dependencies.

How To Install bun.js

curl -fsSL https://bun.sh/install | bash   # for macOS, Linux, and WSL
powershell -c "irm bun.sh/install.ps1|iex" # windows

bun.js to the rescue

And the steps to actually run the script on any of the three operating systems are as follows:

  1. Install bun.js (once)
  2. download your script.js
  3. In your terminal: bun run script.js
  4. bun.js should auto install the dependencies and run the script no problem!

Things to know writing cross-platform bun.js

Add this shebang to the top of the file to let you run the script on mac or linux with just ./script.js

#!/usr/bin/env bun

I couldn't find a builtin bun function to identify the current operating system, but current-os does the trick. Not all codepaths can be exactly the same across platforms, so this package helps facilitate platform-specific behaviors. 

// example usage
var currentOS = require ('current-os'); 
 
// if you are using Windows: 
console.log(currentOS.isWindows); // ==> true
console.log(currentOS.isOSX);     // ==> false
console.log(currentOS.isLinux);   // ==> false

Other stuff:

  • fetch works to get web resources, but you may want to subprocess curl for a faster, more robust download. curl, unlike wget, should be already be present on mac, windows, and linux.
  • You could write the bun script in typescript if you so desired. Bun can run typescript as-is without a compilation step.

Downsides of bun.js

  1. Javascript/node/bun isn't really designed so much for desktop scripting. So things like filesystem manipulation can get a little awkward. We do have Bun.spawnSync({cmd: ["curl", "https://kevbot.xyz", "-o", "out.html"], cwd: os.homedir() }) which is very useful for shelling out commands.
    1. Similarly, I haven't found many useuful js libraries for cli interfaces like fzf or general cli prompting. Maybe they exist, though. Bun overall is usually used a server backend language not so much for desktop scripting, so keep that in mind when looking for libraries. The good thing is that nearly any nodejs package on npm should work on bun.
    2. Bun does have a cross-platform shell-like $ utility https://bun.sh/docs/runtime/shell, which is interesting and available, though.It lets you use commands in your script like await $`cat < filename.txt | wc -c`; on multiple platforms
    3. The requirements aren't version-locked so they could update unexpectedly. I haven't had an issue with this, yet though.
  2. I wouldn't use this scripting pattern for any mission-critical scripts. The goal here is minimizing friction on actually getting the script running. The goal is not necessarily an airtight, well-tested, industrial grade program.

Why not Python?

Something similiar to our bun.js solution could probably be achieved with a uv shebang and inline requirements. I'd like to try it and write a followup article. I get the suspicion that uv/python may be a little more awkward to install than bun on some platforms. uv Installation docs here.

I do write a lot of python scripts, still – and I use them on multiple platforms. It just gets a little awkward once dependencies like click, or requests are introduced. Making a bunch of virtualenvs for a bunch of scripts is a bit awkward.

Why not Node?

lol. lmao, even.

Seriously, though, installing node on windows is harder than bun, and inline requirements are annoying if not impossible?

Also bun supports top level await, while node only sometimes does[4].

In Bun you can import dependencies with import and/or require syntax in the same file. This isn't possible in node as far as I know.

Other Options

  • Nix would probably work well for my own scripts here. Then I could write them in any language I want. Nix is a bit more involved to install on a host system than bun, though, but it's not too bad.
  1. ^

    The desktop application in question: Do I Like the Notetaking Program TriliumNext? (Yes)

  2. ^

    I use yadm to manage my dotfiles. It provides a git interface to all my user configuration files and scripts. My dotfiles can be found here: kleutzinger/dotfiles: My linux config, mostly for Arch linux..

    • My dotfiles are designed for Arch. They're fine on Ubuntu as well.
    • Mac works pretty well with my dotfiles, though there are some path differences ($HOME on mac is /Users/kevin/ rather than /home/kevin/. Yadm is smart though and installs the dotfiles in the correct homedir.
    • On windows, I don't even use yadm – I just clone the dotfiles repo via github desktop and use pieces of it here and there. Though I do also use my dotfiles to good effect inside an Arch linux WSL instance.
  3. ^

    The original script,  update-trilium.sh , runs only on (arch) linux. It also depends on github cli, awk, fzf, and wget.

  4. ^