Bun

Bun v1.1.39


Jarred Sumner · December 17, 2024

We're hiring systems engineers in San Francisco to build the future of JavaScript!

This release fixes 61 bugs (addressing 99 👍). It introduces bun.lock, a new text-based lockfile format. It makes cached bun install 30% faster. It adds support for fetch() request body streams. It brings Node.js' string_decoder, punycode and querystring to 100% Node.js compatibility. It adds a native plugin API for Bun.build(), along with a Rust crate integrating with napi.rs. It implements expect().toMatchInlineSnapshot(). It adds more accurate heap snapshots. It reduces WebSocket server memory usage. It upgrades WebKit bringing Error.isError, faster String.prototype.at, and more.

To install Bun

curl
npm
powershell
scoop
brew
docker
curl
curl -fsSL https://bun.sh/install | bash
npm
npm install -g bun
powershell
powershell -c "irm bun.sh/install.ps1|iex"
scoop
scoop install 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

bun.lock is bun's new text-based lockfile

In this release, we've added a new human-readable lockfile format: bun.lock. This makes git diffs and merge conflicts work better, and unblocks tooling like Dependabot, Renovate, Turbo prune, and more.

To try it out, run:

bun install --save-text-lockfile

We wrote a blog post about bun.lock

Along the way, we also made cached bun install 30% faster. Specifically, when the packages were previously installed and it needs to verify the versions of every installed package to ensure they haven't changed (a common scenario when running bun install locally).

Big thanks to @dylan-conway for implementing this!

fetch() request body streams

You can now send fetch() Request bodies as streams. Previously, only Response bodies could be streamed in the client (Bun.serve() always supported both).

This uses HTTP/1.1 Transfer-Encoding: chunked on the client. When the content length is known ahead of time, it uses Content-Length instead.

Thanks to @cirospaciari for implementing this!

Node.js Compatibility

Fixed: Hang in connection error on postgres package

Two different bugs could lead to a hang in the postgres package:

  • When an AbortSignal passed to net.connect signals, Bun was calling destroy on the net.Socket instead of emitting an abort event. This has been fixed, thanks to @cirospaciari!
  • Calling pause on a net.Socket that came from an upgraded TLS connection could cause the underlying socket to be paused instead of just the node:stream.

Thanks to @cirospaciari for the fix!

100% of Node's string_decoder tests pass

We fixed an edgecase in the "string_decoder" module's handling of incomplete multi-byte characters to align with Node.js, and now all Node.js' string_decoder tests pass.

Thanks to @paperdave!

100% of Node's punycode tests pass

Bun is now fully compatible with Node.js punycode implementation.

Thanks to @snoglobe!

100% of Node's querystring tests pass

We fixed some edgecases in the "querystring" module to align with Node.js.

Thanks to @pfgithub!

Improved node:path test coverage

All but one of Node's path tests now pass (the one for the newly-added method, path.matchesGlob is not implemented yet).

Thanks to @DonIsaac!

Improved node:os test coverage

Only one test failure remains in the os module.

Fixed: crypto.createHash(alg, options) missing option

Thanks to @heimskr!

Fixed: napi_wrap

The napi_wrap function now behaves the same as in Node.js.

Thanks to @190n!

Fixed: error code in node:zlib

Bun was not throwing precisely the same error.code properties for zlib errors as Node.js. This has been fixed, thanks to @nektro!

Fixed: fs.readFileSync with flag parameter in object

The following would incorrectly ignore the flag parameter:

import fs from "fs";
fs.readFileSync("data.txt", { encoding: "utf8", flag: "w+" });

This has been fixed, thanks to @pfgithub!

Bundler improvements

onBeforeParse - Rust/C/Zig plugin API

This release introduces a new zero-copy native plugin API for Bun.build(). Unlike our JavaScript plugin API, this runs inside Bun's threadpool immediately before parsing, without cloning the source code, without undergoing string conversion, and with practically zero overhead.

bun add -g @napi-rs/cli
napi new
cargo add bun-native-plugin

From there, you can implement the onBeforeParse hook:

lib.rs
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;

/// Define the plugin and its name
define_bun_plugin!("replace-foo-with-bar");

/// Here we'll implement `onBeforeParse` with code that replaces all occurrences of
/// `foo` with `bar`.
///
/// We use the #[bun] macro to generate some of the boilerplate code.
///
/// The argument of the function (`handle: &mut OnBeforeParse`) tells
/// the macro that this function implements the `onBeforeParse` hook.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  // Fetch the input source code.
  let input_source_code = handle.input_source_code()?;

  // Get the Loader for the file
  let loader = handle.output_loader();


  let output_source_code = input_source_code.replace("foo", "bar");

  handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX);

  Ok(())
}

To use in Bun.build():

import myNativeAddon from "./my-native-addon";
Bun.build({
  entrypoints: ["./app.tsx"],
  plugins: [
    {
      name: "my-plugin",

      setup(build) {
        build.onBeforeParse(
          {
            namespace: "file",
            filter: "**/*.tsx",
          },
          {
            napiModule: myNativeAddon,
            symbol: "replace_foo_with_bar",
            // external: myNativeAddon.getSharedState()
          },
        );
      },
    },
  ],
});

This plugin API is designed for use with NAPI addons so that existing libraries can add these without thinking much about how to initialize them.

Thanks to @zackradisic for implementing this!

CSS parser improvements

Several CSS parser and printer edge cases have been fixed, thanks to @zackradisic!

Inject environment variables in Bun.build()

This release adds support for injecting environment variables in Bun.build() and bun build:

API
CLI
API
// In Bun.build()
await Bun.build({
  entrypoints: ['./app.tsx'],
  outdir: './out',

  // All environment variables starting with "PUBLIC_"
  // will be injected in the build as process.env.PUBLIC_*
  env: "PUBLIC_*",

  // eg
  // console.log(process.env.PUBLIC_FOO); => "bar"
});

await Bun.build({
  entrypoints: ['./app.tsx'],
  outdir: './out',

  // Inject all environment variables in the build when used.
  env: "inline",
});
CLI
bun build ./app.tsx --env='PUBLIC_*'
bun build ./app.tsx --env=inline

Inline snapshot tests

This release implements expect().toMatchInlineSnapshot() in bun:test. Inline snapshots are stored directly in the test file, and are updated with bun test -u.

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

test("format user profile", () => {
  const result = formatProfile({
    name: "Jarred",
    role: "admin",
  });

  // Snapshot stored directly in test file
  expect(result).toMatchInlineSnapshot(`{
    "displayName": "Jarred",
    "permissions": ["admin"],
    "createdAt": "2024-12-13T00:00:00.000Z"
  }`);
});

test("validate input", () => {
  // Match error messages
  expect(() => validateInput({})).toThrowErrorMatchingInlineSnapshot(`
      "Invalid input:
       - Missing required field: name"
    `);
});

Update snapshots with:

bun test -u

We've also added:

  • expect().toThrowErrorMatchingSnapshot()
  • expect().toThrowErrorMatchingInlineSnapshot()

Thanks to @pfgithub for implementing this!

Memory Profiler improvements

We've implemented support for reporting memory usage from native code in our JavaScriptCore <> Zig class bindings generator. This makes heap snapshots reporting more accurate:

  • Blob, Request, and Response now report the size including their body, URL, and underlying structs instead of only the JavaScript wrapper object sizes.
  • URLSearchParams and Headers now report the size of size of the C++ classes along with the size of each string.
  • ServerWebSocket and WebSocket now reports the size of the underlying structs and buffered data
  • FormData's size now includes the size of each string, the underlying C++ classes, and the size of any files contained in it.
  • Subprocess (Bun.spawn) reports the size of the underlying structs and buffered data
  • Bun.file().writer() reports the size of buffered data
  • All other classes implemented in Zig report their internal struct's size

This does not impact memory usage from the garbage collector's perspective or from the runtime's perspective, but this does make it easier for you to debug what's taking up memory in applications using Bun.

New: estimateShallowMemoryUsageOf in "bun:jsc"

The estimateShallowMemoryUsageOf function in "bun:jsc" estimates the memory usage of a JavaScript value without including the memory usage of its children, properties, or internal slots.

import { estimateShallowMemoryUsageOf } from "bun:jsc";

const obj = { foo: "bar" };
const usage = estimateShallowMemoryUsageOf(obj);
console.log(usage); // => 16

const buffer = Buffer.alloc(1024 * 1024);
estimateShallowMemoryUsageOf(buffer);
// => 1048624

WebSocket server memory reduction

This release reduces the memory usage of Bun's builtin WebSocket server Bun.serve(), particularly for long-running processes that open and close many WebSocket server connections.

In this microbenchmark that opens and closes 200,000 WebSocket connections in loops, the peak RSS drops by 15%.

Microbenchmark

Dependency upgrades

We've updated several dependencies:

  • c-ares to v1.34.3
  • lshpack to v2.3.3
  • libdeflate to v1.22
  • SQLite to 3.470.200
  • BoringSSL

WebKit Upgrade

This release upgrades the internal version of JavaScriptCore from upstream WebKit, which includes improvements to loop unrolling, Intl.PluralRules, and Intl.NumberFormat, and more.

Error.isError

The Error.isError(object) method returns true if the object is really an Error instance.

Error.isError(new Error()); // => true
Error.isError({}); // => false
Error.isError(new Error("foo")); // => true
Error.isError(new Error("foo").message); // => false
Error.isError({ [Symbol.toStringTag]: "Error" }); // => false
Error.isError(new (class Error {})()); // => false
Error.isError({ constructor: function Error() {} }); // => false

Error.isError is a stage3 TC39 proposal.

Faster String.prototype.at

Bug Fixes

Fixed: Debugger flakiness in VSCode

The process would sometimes exit before the debugger could attach. Please let us know if you're still experiencing this!

This has been fixed, thanks to @RiskyMH

Fixed: Crash in POSIX signal handling

A crash or deadlock that could occur occur after registering a signal handler like process.on('SIGINT') or process.on('SIGTERM') and repeatedly receiving signals has been fixed.

Fixed: Rare file descriptor leak in Bun Shell and Bun.spawn

A theoretical file descriptor leak in Bun Shell and Bun.spawn has been fixed. This could potentially happen when spawning processes on other threads in older versions of Linux.

It turns out we were also not always correctly marking file descriptors as non-blocking, which also has been fixed.

Fixed: fetch redirect handling with "Connection: close"

When fetch() redirects with "Connection": "close", Bun could potentially report a connection error instead of handling the redirect as expected. Thanks to @cirospaciari for the fix!

Fixed: Crash in bun install with invalid package.json in workspace

A crash that could occur when running bun install on a workspace with invalid package.json files has been fixed.

Fixed: rare crash in Bun.build() plugins

A rare crash that could occur in Bun.build() plugins has been fixed. This would potentially happen if using the "file" loader in a plugin.

Fixed: crash in Bun.build() with a lot of comments in TypeScript files

A crash that could occur in certain cases when many comments were present in TypeScript files has been fixed, thanks to @dylan-conway!

Fixed: global .npmrc not using auth

Auth configuration was only applied per .npmrc file and then discarded. Therefore, auth defined in the global .npmrc wouldn't apply to registries in the project .npmrc.

This has been fixed, thanks to @robertshuford!

Thanks to 20 contributors!