Bun

Bun v1.0.15


Ashcon Partovi · December 2, 2023

Bun v1.0.15 fixes 23 bugs (addressing 117 👍 reactions), tsc starts 2x faster, prettier 40% faster. Stable WebSocket client, syntax-highlighted errors, cleaner stack traces, add custom test matchers with expect.extend() + additional expect matchers, TensorFlow.js support, and more.

Bun is an incredibly fast JavaScript runtime, bundler, transpiler, and package manager — all in one. In case you missed it, here are some of the recent changes to Bun:

  • v1.0.12 - Adds bun -e for evaluating scripts, bun --env-file for loading environment variables, server.url, import.meta.env, expect.unreachable(), improved CLI help output, and more
  • v1.0.13 - Fixes 6 bugs (addressing 317 👍 reactions). 'http2' module & gRPC.js work now. Vite 5 & Rollup 4 work. Implements process.report.getReport(), improves support for ES5 'with' statements, fixes a regression in bun install, fixes a crash when printing exceptions, fixes a Bun.spawn bug, and fixes a peer dependencies bug
  • v1.0.14 - Bun.Glob, a fast API for matching files and strings using glob patterns. It also fixes a race condition when extracting dependencies during bun install, improves TypeScript module resolution in node_modules, and makes error messages easier to read.

To install Bun:

curl
npm
brew
docker
curl
curl -fsSL https://bun.sh/install | bash
npm
npm install -g bun
brew
brew tap oven-sh/bun
brew install bun
docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

To upgrade Bun:

bun upgrade

Transpiler cache makes CLIs like tsc up to 2x faster

Bun lets you run TypeScript and JSX files without a build step. This works by running Bun's JavaScript transpiler before files load. In this release, we're introducing a content-addressable cache for files larger than 50KB, to avoid the performance overhead of transpiling the same files repeatedly. This makes CLIs, such as tsc, run up to 2x faster.

tsc --help gets a 2x speedup compared to Bun v1.0.14:

Benchmark 1: bun --bun ./node_modules/.bin/tsc --help
  Time (mean ± σ):      82.2 ms ±   2.6 ms    [User: 70.1 ms, System: 14.3 ms]
  Range (min … max):    78.4 ms …  87.1 ms    37 runs

Benchmark 2: bun-1.0.14 --bun ./node_modules/.bin/tsc --help
  Time (mean ± σ):     197.0 ms ±   3.6 ms    [User: 172.0 ms, System: 27.2 ms]
  Range (min … max):   192.4 ms … 204.4 ms    14 runs

Benchmark 3: node ./node_modules/.bin/tsc --help
  Time (mean ± σ):     113.8 ms ±   3.2 ms    [User: 103.6 ms, System: 16.0 ms]
  Range (min … max):   110.0 ms … 123.4 ms    23 runs

Summary
  bun --bun ./node_modules/.bin/tsc --help ran
    1.38 ± 0.06 times faster than node ./node_modules/.bin/tsc --help
    2.40 ± 0.09 times faster than bun-1.0.14 --bun ./node_modules/.bin/tsc --help

Prettier gets up to 40% faster (compared to Bun v1.0.14)

Benchmark 1: bun --bun ./node_modules/.bin/prettier --write ./examples/hashing.js
  Time (mean ± σ):     124.5 ms ±   3.2 ms    [User: 144.5 ms, System: 23.4 ms]
  Range (min … max):   119.9 ms … 131.8 ms    23 runs

Benchmark 2: bun-1.0.14 --bun ./node_modules/.bin/prettier --write ./examples/hashing.js
  Time (mean ± σ):     184.4 ms ±   4.1 ms    [User: 202.7 ms, System: 28.6 ms]
  Range (min … max):   175.2 ms … 192.9 ms    15 runs

Benchmark 3: node ./node_modules/.bin/prettier --write ./examples/hashing.js
  Time (mean ± σ):     162.9 ms ±   3.7 ms    [User: 161.8 ms, System: 42.5 ms]
  Range (min … max):   158.2 ms … 170.7 ms    17 runs

Summary
  bun --bun ./node_modules/.bin/prettier --write ./examples/hashing.js ran
    1.31 ± 0.04 times faster than node ./node_modules/.bin/prettier --write ./examples/hashing.js
    1.48 ± 0.05 times faster than bun-1.0.14 --bun ./node_modules/.bin/prettier --write ./examples/hashing.js

To minimize disk usage, the transpiler cache is global and shared across all projects. It's safe to delete the cache at any time, and since it's content-addressable it will not contain duplicate entries. If you are running Bun with an emphemeral filesystem, such as Docker, it is recommended to disable the cache.

If you want to customize the path where these files are cached, you can set the BUN_RUNTIME_TRANSPILER_CACHE_PATH environment variable. You can also set the value to 0 to disable the cache entirely.

Stable WebSocket client

This release includes a (finally) stable WebSocket client. Bun has supported WebSocket for a while, but it had various protocol bugs, such as disconnecting early or not handling message fragmentation properly. Now, it is stable and should work with most WebSocket servers.

const ws = new WebSocket("wss://echo.websocket.org/");

ws.addEventListener("message", ({ data }) => {
  console.log("Received:", data);
});

ws.addEventListener("open", () => {
  ws.send("Hello!");
});

To test compliance, we used the Autobahn test suite, which is the de-facto standard for testing WebSocket implementations. Bun passed 100% of Autobahn's tests, excluding those that test for compression. (our WebSocket client does not support compression yet, but will soon.)

That means all of these issues, and more, have now been fixed.

Thanks to @cirospaciari for fixing these bugs and getting Autobahn test compliance!

expect.extend() and more matchers

You can now defined custom test matchers using expect.extend(). This is useful when you want to create a custom matcher that is reusable across multiple tests. For example, you can create a custom matcher that checks if a number is within a range:

import { test, expect } from "bun:test";

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false,
      };
    }
  },
});

test("toBeWithinRange()", () => {
  expect(1).toBeWithinRange(1, 99); // ✅
  expect(100).toBeWithinRange(1, 99); // ❌ expected 100 to be within range 1 - 99
});

Bun also supports more asymmetric matchers, including:

In addition to the above asymetric matchers, Bun is introducing two new asymetric matchers that work for promises.

  • expect.resolvesTo
  • expect.rejectsTo

For example, you can use expect.resolvedTo to check for a promise that resolves to a value:

import { test, expect } from "bun:test";
import { getTempurature, getForecast } from "./weather";

test("expect.resolvedTo", async () => {
  const weather = {
    tempurature: getTempurature(),
    forecast: getForecast(),
  };

  await expect(weather).toMatchObject({
    tempurature: expect.resolvedTo.closeTo(10, 5),
    forecast: expect.resolvedTo.stringMatching(/rain|snow|sleet/),
  });
});

Thanks to @otgerrogla for implementing these features and also fixing various bugs with expect().

There are also more mock matchers available, including:

You can use these matchers to check if a function was called with certain arguments:

import { test, mock, expect } from "bun:test";

test("toHaveBeenCalledWith()", () => {
  const fn = mock(() => {});
  sum(1, 2, 3);

  expect(fn).toHaveBeenCalledWith(1, 2, 3); // ✅
  expect(fn).toHaveBeenCalledWith(1, 2); // ❌
});

Thanks to @james-elicx for implementing these matchers and making various error messages better.

Syntax-highlighting for errors

When an exception occurs in Bun, it prints a stack trace to the console with a multi-line source code preview. Now that source code preview gets syntax highlighted, which makes it easier to read.

Syntax errors

The syntax highlighter also extends to all other errors that include source code, such as this one when using bun install with multiple workspace packages sharing the same name.

Thanks to @paperdave for working on this!

Better Error.stack traces

We've made several improvements to how Error.stack traces are formatted.

Stack traces now include less noise, such as internal functions that are not relevant to the error.

1 | throw new Error("Oops");
          ^
error: Oops
    at /Users/jarred/Desktop/oops.js:1:7
-   at globalThis (/Users/jarred/Desktop/oops.js:3:14)
-   at overridableRequire (:1:20)
    at /Users/jarred/Desktop/index.js:3:8
-   at globalThis (/Users/jarred/Desktop/index.js:3:8)

We also fixed a bug where Bun would not detect if the Error.stack property was modified. This can happen if you use a library that modifies the stack trace, such as concatenating multiple traces together.

const err = new Error("Oops 1");
err.stack += "\n" + new Error("Oops 2").stack;
throw err;

Bun now re-parses error.stack, so that modified stack traces are properly detected and formatted.

recursive in fs.readdir() is 40x faster than Node.js

Bun now supports the recursive option in fs.readdir(), which is used to recursively read a directory.

import { readdir } from "fs/promises";
const results = await readdir(__dirname, { recursive: true });
console.log(results); // ["a.js", "b/c.js" ...

When running readdir() in the test/ directory of Bun's repository, on Linux it's 40x faster than Node.js v21.2.0.

CommonJS modules get 1% faster

We made some improvements to how Bun loads CommonJS modules, which makes them 1% faster.

This is because we simplified our internal wraper functions for CommonJS. Previosuly, our wrapper functions were written in JavaScript, with some help from Bun's transpiler. Now, this is done in native code, so it's faster and uses less memory.

Thanks to @paperdave for implementing this!

TensorFlow.js now works

There was an outstanding bug in Bun's implementation of napi_create_string_utf16 and napi_create_arraybuffer that prevented TensorFlow.js from working. This has now been fixed.

import * as tf from "@tensorflow/tfjs-node";

const model = tf.sequential({
  layers: [
    tf.layers.dense({ units: 128, activation: "relu", inputShape: [1] }),
    tf.layers.dense({ units: 3 }),
    tf.layers.softmax(),
  ],
});

model.compile({
  optimizer: "adam",
  loss: "categoricalCrossentropy",
  metrics: ["categoricalAccuracy"],
});

const result = model.predict(tf.tensor([0]));
if (Array.isArray(result)) throw new Error("Expected a single tensor");
const prediction = await result.data();

console.log(prediction); // Float32Array(3) [...]

Support for crypto.sign and crypto.verify

Bun now supports sign and verify in the node:crypto module, which can be used to sign and verify data using a private key.

import { sign, generateKeyPairSync } from "node:crypto";

const { privateKey } = generateKeyPairSync("ed25519");
const signature = sign(undefined, Buffer.from("foo"), privateKey);

console.log(signature); // Buffer(64) [ ... ]

Thanks to @cirospaciari for implementing this feature!

bun install migration from package-lock.json v2

Bun is now able to migrate package-lock.json files that have a lockfileVersion of 2. This is useful if you are migrating from npm to Bun.

❯ cat package-lock.json | jq .lockfileVersion
2

❯ bun install
bun install v1.0.15
[3.57ms] migrated lockfile from package-lock.json
 + svelte@4.0.0 (v4.2.8 available)

 21 packages installed [265.00ms]

bun install duplicate workspace bugfix

An error regarding duplicate workspace packages has been fixed, thanks to @dylan-conway.

Trailing commas with console.log

Bun now includes trailing commas for objects that are printed using console.log. This makes it easier to copy-paste the output from your console, do a search-and-replace, and matches what code formatters like prettier do.

console.log({ a: 1, b: 2, c: "3" });
{
   a: 1,
   b: 2,
-  c: "3"
+  c: "3",
}

Thanks to @hustLer2k for implementing this improvement, and to @ArnaudBarre for the suggestion!

Implemented: console.timeLog

There was a bug that prevented console.timeLog from working. This was fixed thanks to @lqqyt2423.

Fixed: Detached usage of ReadableStream

There was a bug where a undefined is not an object error would throw when a ReadableStream was detached, which has now been fixed.

const { exited, stdout } = Bun.spawn(["echo", "hello"], {
  stdout: "pipe",
});

await exited;

// Since the process has exited, the `stdout` ReadableStream is detached.
console.log(await Bun.readableStreamToText(stdout));

// Before: error: undefined is not an object
// After: "hello\n"

Fixed: fs.opendir() has path property

Previously, fs.opendir() did not have a path property, this has now been fixed thanks to @samfundev.

import { test, expect } from "bun:test";
import { opendir } from "fs/promises";

test("opendir() has `path` property", () => {
  const result = await opendir(".");
  expect(result).toHaveProperty("path", ".");
});

Fixed: Duplicate Content-Range header

There was a bug where Bun would send duplicate Content-Range headers.

const file = Bun.file("video.mp4");
const start = 0;
const end = 1000;
const range = file.slice(start, end);

return new Response(range, {
  status: 206,
  headers: {
    "Content-Range": `bytes ${start}-${end}/${file.size}`,
  },
});

Bun would not properly detect that a Content-Range header was already set, and would send a duplicate header.

content-range: bytes 0-1000/10000
- content-range: bytes 0-1000/*

This was fixed thanks to @libersoft-org.

Fixed: Various transpiler bugs

Hyphenated keys in tsconfig.json

There was a bug where if a tsconfig.json included a key that contained hyphens, parsing would fail. This was fixed thanks to @DontBreakAlex.

tsconfig.json
{
  "key-with-hyphens": "value"
}

Spreading elements in JSX

There was a bug where spreading a child element into a parent element would cause an unexpected error. This was fixed thanks to @rhyzx.

Input:

const a = <h1>{...[123]}</h1>;

Output:

- error: Unexpected ...
+ var a = jsx_dev_runtime.jsxDEV("h1", {
+  children: [...[123]]
+ }, undefined, true, undefined, this);

Stack Overflow in already-minified code

"Stack Overflow" is not just a website. Programs execute code in a stack, and if the stack uses too much memory, it overflows.

Bun's parser is a recursive descent parser, which means it uses recursion to parse code. This makes the implementation simpler, but it can lead to stack overflow if the input source code is too deeply nested with binary expressions. This release fixes that.

For example, if you were to write a file with contents like this:

const chain =
  `globalThis.a = {};` +
  "\n" +
  `globalThis.a + globalThis.a +`.repeat(1000000) +
  `globalThis.a` +
  "\n";

await Bun.write("stack-overflow.js", chain);

And then run it with bun stack-overflow.js, it would crash in bun's transpiler due to stack overflow.

This has now been fixed, thanks to @dylan-conway.

This bug manifested when using Bun's runtime to format a TypeScript file with Prettier.

override keyword in TypeScript constructor

There was a bug where the override keyword in a TypeScript constructor would cause an unexpected error.

class Foo {}
class Bar extends Foo {}

class Fizz {
  constructor(readonly foo: Foo) {}
}

class Buzz extends Fizz {
  constructor(override bar: Bar) {
    super(foo);
  }
}

new Buzz(new Bar());
- 10 |    constructor(override foo: FooChild) {
-                             ^
- error: Expected ")" but found "foo"
-    at example.ts:10:23

Fixed: Dynamicly loaded CommonJS modules in bun build --compile

When the argument to require is not statically analyzable, bun build leaves it as is.

const dynamic = (name) => require(name);

console.log(dynamic("some-package"));

If you used this with bun build --compile and loaded a commonjs file this way, the transpiler would generate code with a syntax error. This has been fixed.

Thanks to 25 contributors!

We're always excited to welcome new contributors to Bun. 12 new contributors made their first contribution in this release.

Thanks to the 25 contributors who made this release possible: