Using Bun.js as a bundler

Bun.js is a new (as of 2023) JavaScript runtime that is still very much in development, with it’s primary focus being on extreme speed. I’ve been following it for a while but until today haven’t had a good excuse to use it.

(Edit: There’s some good conversation about this post on Hacker News here)

The author, Jarred Sumner, announced on Twitter today that they have shipped a beta version of a new code bundler for Bun, showing some crazy speed increases over other bundlers. This piqued my interest, as I use a combination of Webpack, Browserify and Uglify on my side projects, in this case my tablet PWA that I built for my kids kidzfun.art, which work but are really slow.

My current workflow can result in a 5 – 7 second wait for all my JS files to rebuild when I save a file, and I thought that Bun could help with this. It turns out I was right! …. with caveats.

You can see the docs for Bun.build() at https://bun.sh/docs/cli/build , and they are well written and quite comprehensive.

My requirements were to

  • Build multiple files quickly, each of which imports multiple other 3rd party files from node_modules.
  • Build minified and non-minified files
  • The resulting file can be included directly in a browser using a <script> tag.

Getting started

I started off by running default build code (for Bun v0.6.1)

const myFiles = [...];

await Bun.build({
  entrypoints: [myFiles],
  outdir: './build'
});

by adding a script to my package.json file

 "build-browser": "bun scripts/build-browser.js"

and this worked just fine. More importantly, it was crazily fast. Instead of 5 seconds it now seemed to finish as the Enter key was still traveling back upwards from executing the command. Nice!

Minification

Minification looks simple in the docs, but unfortunately it’s where the beta nature of Bun shows up. Running the code above with minification

const myFiles = [...];

await Bun.build({
  entrypoints: [myFiles],
  outdir: './build',
  minify: true
});

results in an error being thrown that shuts down the process if there is more than one entry point file.

Bus error: 10

Searching the web didn’t turn up anything, but the solution is to only pass a single entry point file path to Bun.build() if you are minifying the code. Throw that in a for loop to get through all the files and it runs just fine!

A second issue with the default minification is that it broke my app in strange ways that I could not track down – I’m guessing that it’s rewriting the code in some way that is not fully stable yet. I solved it by turning off the syntax minification option

const myFiles = [...];

await Bun.build({
  entrypoints: [myFiles],
  outdir: './build',
  minify:{
    whitespace: true,
    identifiers: true,
    syntax: false // Setting this to false fixes the issue
  }
});

Removing Exports

Bun inserts code that looks like this at the bottom of the built file, in this case from a file called account.ts

var account_default = {};
export {
  account_default as default
};

If you load this in a browser <script> tag it will throw an error. I couldn’t find a way to tell Bun how to not output this, so I had to write a relatively simple function to detect this at the end of each output file and remove it.

Watch issues

I have some code that uses the node-watch module to automatically re-run the build when a file changes. Under the hood this uses the fs.watch function, which it turns out Bun does not yet support. Here’s the Github issue tracking it. I tried to use the native Bun watch functionality, but this executed the script code which is not what I’m looking for.

I came up with a hacky solution that works fairly well, where I use the RunOnSave extension for VS Code to execute

touch ./.last_modified_timestamp

every time I save a file. Then in my build script I use setInterval to check the last modified time of this file and re-run the build if it has changed. Hacky but it works. Hopefully Bun will implement fs.watch soon and I can throw out this code.

function build() {
  ...
}

const timestampFilePath = `${rootDir}/.last_modified_timestamp`;
if (fs.existsSync(timestampFilePath)) {
  let lastModifiedRootFolder = 0;
  setInterval(() => {
    const stat = fs.statSync(timestampFilePath);
    if (stat.mtime.getTime() !== lastModifiedRootFolder) {
      lastModifiedRootFolder = stat.mtime.getTime();
      build();
    }
  }, 500);
}

Vercel build failures

Once everything was running just fine locally on my Mac, I pushed the branch to Github so Vercel would build it (it’s a NextJS application). This threw up a new issue. My build script uses the native Node exec() function to move and copy files. This works just fine on my Mac, but when running the build in the cloud environment all these calls would fail. There’s something unfinished with Bun’s implementation of the child_process module that breaks when run in the Vercel build environment.

My solution to this was to simply change all these execSync calls to use the Node fs functions, e.g.

import fs from 'fs';
....
fs.copyFileSync(srcPath, destPath);
fs.renameSync(path, `${rootDir}/public/${fileName}`);

Epilogue

After a few hours of work, reading up on Bun and working my way through these issues, I now have a much simpler build system that runs in the blink of an eye. My Vercel build times have reduced from 2 minutes to just 50 seconds (that’s all React stuff & fetching node_modules). My watch script runs in a few milliseconds instead of 5 or more seconds, My code is much simpler and I’ve removed Webpack, Browserify and Uglify from my projects.

Thanks so much to the Bun team for a great project. Even as early as it is at time of writing (mid 2023), it’s highly useful, and as they work through all the kinks it will only get more so. I look forward to using it more in the months and years to come!

… oh you’re still here?

The project I sped up using Bun is KidzFun.art, the iPad/tablet app I built for my kids. If you have young kids who

  • like to draw & colour,
  • do maths problems,
  • want to make Gifs from their drawings,
  • should never be shown ads, and
  • somehow care about the open web

have then try out my progressive web app 🙂