---
title: Bun v1.4.4
description: "Fixes 271 issues, addressing 544 👍. bun fmt, a Prettier-compatible formatter built into Bun that is 3x to 6x faster than oxfmt, bun check is 1.4x to 7.8x faster on large projects, bun install migrates Yarn Berry lockfiles, retries with exponential backoff and closes 133 issues, up to 3.2x faster hoisted installs from a warm cache on Linux, bun pm fetch, bun publish --recursive, packageExtensions, Bun.SQL uses up to 48% less CPU per query, query.cancel(), COPY streams, column name transforms, user-defined functions and update hooks in bun:sqlite, ws rewritten to behave like npm ws on Node.js, ws.pause() and ws.resume() in Bun.serve, a JavaScriptCore upgrade with 54x faster toLocaleString, up to 2.8x faster Headers iteration, up to 2.6x faster response.body streams, extglob and brace ranges in Bun.Glob, a redesigned error overlay in the dev server, and many bugfixes and Node.js compatibility improvements."
date: "2026-10-22T00:58:24.788Z"
author: jarred
---

#### To install Bun

{% codetabs %}

```sh#curl
$ curl -fsSL https://bun.sh/install | bash
```

```sh#npm
$ npm install -g bun
```

```sh#powershell
$ powershell -c "irm bun.sh/install.ps1|iex"
```

```sh#scoop
$ scoop install bun
```

```sh#brew
$ brew tap oven-sh/bun
$ brew install bun
```

```sh#docker
$ docker pull oven/bun
$ docker run --rm --init --ulimit memlock=-1:-1 oven/bun
```

{% /codetabs %}

#### To upgrade Bun

```sh
$ bun upgrade
```

## `bun fmt`: a formatter built into Bun

`bun fmt` is an incredibly fast, Prettier-compatible code formatter that works with your existing Prettier or oxfmt config.

```sh
$ bun fmt
src/index.ts
src/styles.css
Formatted 2 files, 2 unchanged [5.59ms]
```

It formats JavaScript, TypeScript, JSX, Flow, CSS, SCSS, Less, HTML, Vue, Svelte, Angular, Lightning Web Components, MJML, JSON, JSONC, JSON5, YAML, TOML, Markdown, MDX, GraphQL and Handlebars. Embedded code is formatted too, like `<style>` in HTML, code blocks in Markdown and `` css`...` `` templates in JavaScript.

In Prettier's own repository (2,616 files, with their config), `bun fmt --check` takes 60 ms and Prettier takes 17 seconds:

| Tool            |    Time | Peak memory |
| --------------- | ------: | ----------: |
| `bun fmt`       | 59.7 ms |       48 MB |
| Prettier 3.9.10 |  17.1 s |      459 MB |

That's 286x faster. On vitest, date-fns, monkeytype and h3, it's 234x to 425x faster than Prettier 3.9.9.

On projects that already use oxfmt, with their own config, it's 3x to 6x faster than oxfmt 0.74.0:

| Project     | Files | `bun fmt` | oxfmt 0.74.0 | Faster |       Memory |
| ----------- | ----: | --------: | -----------: | -----: | -----------: |
| RSSHub      | 7,121 |   97.3 ms |     298.5 ms |   3.0x |  42 vs 95 MB |
| redocly-cli | 3,397 |   62.4 ms |     217.5 ms |   3.4x | 61 vs 107 MB |
| vite        | 2,537 |   65.2 ms |     406.7 ms |   6.2x | 64 vs 137 MB |
| fumadocs    | 2,452 |   44.6 ms |     1,395 ms |    31x | 48 vs 179 MB |

Both tools see the same files, exit 0 and leave the tree unchanged.

{% details summary="Reproduce these numbers" %}

Each project is a fresh clone at a pinned commit. The oxfmt projects have no `node_modules`.

```sh#vite
$ bun install -g oxfmt@0.74.0
$ git clone https://github.com/vitejs/vite && cd vite
$ git checkout 4ca25e1d3a24
$ hyperfine --warmup 2 "oxfmt ." "bun fmt ."
```

```sh#redocly-cli
$ git clone https://github.com/Redocly/redocly-cli && cd redocly-cli
$ git checkout 82da67d6ea5b
$ hyperfine --warmup 3 --runs 20 "oxfmt --check ." "bun fmt --check ."
```

```sh#RSSHub
$ git clone https://github.com/DIYgod/RSSHub && cd RSSHub
$ git checkout 8776927c78de
$ hyperfine --warmup 3 --runs 20 "oxfmt --check ." "bun fmt --check ."
```

```sh#fumadocs
$ git clone https://github.com/fuma-nama/fumadocs && cd fumadocs
$ git checkout bc86bbb0d406
$ hyperfine --warmup 3 --runs 20 "oxfmt --check ." "bun fmt --check ."
```

```sh#prettier
$ git clone https://github.com/prettier/prettier && cd prettier
$ git checkout df39b5b60a28
$ bun install --ignore-scripts
$ hyperfine --warmup 3 "bun fmt --no-script . --check" "node node_modules/prettier/bin/prettier.cjs . --check"
```

The same runs on one core, and on only vite's JavaScript and TypeScript files (`'**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'`):

| Run                         | `bun fmt` | oxfmt 0.74.0 | Faster |
| --------------------------- | --------: | -----------: | -----: |
| vite, 1,608 JS and TS files |   52.2 ms |     188.3 ms |   3.6x |
| RSSHub, one core            |  621.6 ms |     1,212 ms |   1.9x |
| redocly-cli, one core       |  375.1 ms |     645.5 ms |   1.7x |
| fumadocs, one core          |  255.2 ms |     5,608 ms |    21x |

- vite: 16-core M4 Max MacBook Pro, macOS 26.7. Mean.
- Everything else: Intel Xeon Platinum 8488C, 8 physical cores pinned with `taskset`, Amazon Linux 2023, xfs on NVMe, warm file cache. Median.

{% /details %}

`bun fmt` wouldn't exist without [Prettier](https://github.com/prettier/prettier), [Biome](https://github.com/biomejs/biome) and [Oxc](https://github.com/oxc-project/oxc). It's a port of Prettier, and the JavaScript and TypeScript printer is a port of [`oxc_formatter`](https://github.com/oxc-project/oxc/tree/main/crates/oxc_formatter), which is a port of Biome's formatter.

Thank you to the Prettier, Biome and Oxc teams for years of great work.

Read more in the [docs](/docs/format).

### `bun fmt --check`

`--check` lists the files that need formatting without writing anything. It exits with code 1 if there are any.

```sh
$ bun fmt --check
src/index.ts
src/styles.css
2 files need formatting. Run bun fmt to fix them.
```

### `bun fmt --staged` and `--changed`

`--staged` only formats the files you're about to commit and re-stages them, so you don't need `lint-staged`.

```sh#.git/hooks/pre-commit
#!/bin/sh
bun fmt --staged
```

`--changed` formats the files that differ from your default branch. Pass a ref to compare against something else.

```sh
$ bun fmt --changed
$ bun fmt --changed=origin/dev
```

### Use your Prettier or oxfmt config

`bun fmt` reads `.prettierrc*`, `prettier.config.*`, `"prettier"` in `package.json`, `overrides`, `.editorconfig` and `.prettierignore`. It also reads `.oxfmtrc.json`, `.oxfmtrc.jsonc` and `oxfmt.config.ts` (or `.mts`). It accepts the same CLI flags as Prettier or oxfmt, and `// prettier-ignore` comments work.

```json-diff
  "scripts": {
-   "format": "prettier --write ."
+   "format": "bun fmt"
  }
```

Import sorting is built in, so you can leave `@trivago/prettier-plugin-sort-imports`, `@ianvs/prettier-plugin-sort-imports` and `prettier-plugin-organize-imports` in `plugins`. Languages that need a Prettier plugin, like Astro, fall back to the Prettier installed in your project.

The command is `bun fmt`, like `cargo fmt`, `go fmt`, `deno fmt` and `zig fmt`. `bun format` still runs your `format` script.

`bun fmt --doctor` shows how Bun interprets your config and which packages you can uninstall.

### Options in `bunfig.toml`

All of Prettier's options are supported under the same names. Each one is also available as a flag, like `--print-width 100` or `--no-semi`. `bun fmt --init` creates the `[fmt]` section.

```toml#bunfig.toml
[fmt]
printWidth = 100
semi = false
sortImports = true
sortTailwindcss = true
sortPackageJson = true
jsdoc = true

[[fmt.overrides]]
files = ["*.test.ts"]
options = { printWidth = 120 }
```

`sortImports`, `sortTailwindcss`, `sortPackageJson` and `jsdoc` are off by default, and don't need a Prettier or oxfmt config. `sortImports` groups imports, sorts the names in `{ }` and merges duplicate imports from the same module.

In a monorepo, `bun fmt` walks up from the working directory to the nearest `bunfig.toml` with a `[fmt]` section. Without a Prettier config, `bun fmt` also formats TOML. It skips `*.min.js`, `*.min.css` and lockfiles unless you pass them explicitly.

### Prettier's Tailwind, `package.json` and Svelte plugins are built in

`bun fmt` does the work of `prettier-plugin-tailwindcss`, `prettier-plugin-packagejson` and `prettier-plugin-svelte`, so you don't need them installed. You can leave them in `plugins`, and their options are supported.

```tsx
// Before
<button className="text-white px-4 sm:px-8 py-2 bg-sky-700 hover:bg-sky-800" />
// After
<button className="bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 sm:px-8" />
```

Tailwind classes are sorted with your project's `tailwindcss`, version 3 or 4. If it isn't installed, they're left as they are.

We compared the output against Prettier 3.9.9 with version 0.8.1 of the plugin, on Tailwind 4.3.3 and 3.4.19.

| Input                                                                        |  Total | Identical |     % |
| ---------------------------------------------------------------------------- | -----: | --------: | ----: |
| JavaScript and TypeScript files                                              | 13,952 |    13,654 | 97.9% |
| HTML, Vue, Angular, LWC, Handlebars, CSS, SCSS, Less, Markdown and MDX files |  2,176 |     2,148 | 98.7% |
| Tailwind 3 projects                                                          |     15 |        15 |  100% |

About half of the differences are intentional. In `` className={`header${open ? " active" : ""}`} ``, the plugin trims `" active"` to `"active"`, so the element ends up with the class `headeractive`. `bun fmt` keeps the space.

For `package.json`, keys are sorted in the same order as the plugin and values are left untouched. Of 13,670 real-world `package.json` files, 13,644 (99.8%) are byte-for-byte identical to the output of Prettier 3.9.9 with the plugin. 20 differ only in values that Bun preserves, and the plugin throws on the other 6.

`.svelte` files are formatted by default, and the output matches `prettier-plugin-svelte` 4.1.1. With a Prettier config, they're formatted if the config lists the plugin. To skip them, set `svelte = false` in `[fmt]` or pass `--no-svelte`.

If your config uses a plugin that `bun fmt` doesn't support, like `prettier-plugin-jsdoc`, it prints an error naming the plugin, skips the files the plugin applies to and exits with code 2.

### Where `bun fmt` intentionally differs from Prettier

Prettier's output changes the meaning of the code for these inputs. `bun fmt` preserves it.

| Input                                          | Prettier 3.9.9                                       | `bun fmt` |
| ---------------------------------------------- | ---------------------------------------------------- | --------- |
| `font-family: Font\ Awesome` in CSS            | `Font\Awesome`, where `\A` is a hex escape           | Kept      |
| `x \|b` in a CSS selector                      | `x\|b`, a namespace selector instead of a descendant | Kept      |
| `<!DOCTYPE html>` in Handlebars                | Dropped                                              | Kept      |
| A `<script>` with a `// comment` in Handlebars | Lines joined, so the comment swallows the next line  | Kept      |

`bun fmt --debug-check` re-parses each file after formatting and fails if the meaning changed, like Prettier's `--debug-check`.

### Testing `bun fmt`

Bun's CI runs Prettier 3.9.9's own formatting tests on every commit. A test passes when the output is byte-for-byte identical to Prettier's.

64 of the 83 differences are one intentional case: Prettier drops trailing blank lines from a YAML block scalar, which changes the string.

| Language        |     Tests | Identical |         % |
| --------------- | --------: | --------: | --------: |
| JavaScript      |     1,357 |     1,356 |     99.9% |
| JSX             |       186 |       186 |      100% |
| TypeScript      |       756 |       756 |      100% |
| Flow            |     1,505 |     1,503 |     99.9% |
| JSON            |       785 |       785 |      100% |
| CSS             |       162 |       160 |     98.8% |
| Less            |       238 |       236 |     99.2% |
| SCSS            |       136 |       135 |     99.3% |
| GraphQL         |       104 |       102 |     98.1% |
| YAML            |     1,621 |     1,554 |     95.9% |
| Markdown        |     1,572 |     1,569 |     99.8% |
| MDX             |        32 |        32 |      100% |
| HTML            |       272 |       269 |     98.9% |
| Vue             |       189 |       189 |      100% |
| Angular         |       109 |       109 |      100% |
| Handlebars      |       128 |       128 |      100% |
| MJML, LWC, misc |       579 |       579 |      100% |
| **Total**       | **9,731** | **9,648** | **99.1%** |

Another 90 tests are skipped. 66 of them use syntax proposals at stage 2 or below that only Babel parses, and 13 need a Prettier plugin.

With an oxfmt config, `bun fmt` matches oxfmt 0.72's output on 1,343 of oxfmt's 1,498 fixtures (89.7%).

On 330 open-source repositories, we ran each project's format command with its own formatter and with `bun fmt`. A run only counts as identical if every file is byte-for-byte the same. Next.js counts as a mismatch: 26,334 files are identical, but `bun fmt` rejects one file that uses a Babel-only proposal.

| Compared with | Runs | Identical |     % |
| ------------- | ---: | --------: | ----: |
| oxfmt 0.72    |  240 |       227 | 94.6% |
| Prettier      |   74 |        57 | 77.0% |

Most of the Prettier mismatches come from an older version of Prettier, Prettier plugins or Markdown files.

<!-- https://github.com/oven-sh/bun/commit/c39c718b44107075481f80a9267259059565d504 -->

## New in `bun check`

### `bun check` is 1.4x to 7.8x faster on large projects

Cold runs with 8 threads on a Linux x64 machine, best of 3. Both versions report the same number of errors on each project.

| Project           | Bun v1.4.4 | Bun v1.4.3 | Faster |
| ----------------- | ---------: | ---------: | -----: |
| MUI `docs`        |     1.31 s |    10.28 s |   7.8x |
| t3code `server`   |     1.62 s |     4.59 s |   2.8x |
| Supabase `studio` |     1.27 s |     2.84 s |   2.2x |
| mikro-orm         |     1.82 s |     3.30 s |   1.8x |
| VS Code           |     2.82 s |     4.09 s |   1.4x |

Peak memory on MUI's `docs` drops from 11.2 GB to 1.3 GB. The other four use up to 0.3 GB more.

Of 76 other repositories, 69 are more than 10% faster. Bun's own `src/js` is slower: 0.20 s, up from 0.11 s.

- A `switch` with thousands of cases, or a variable with thousands of assignments that's read from closures, no longer takes seconds to check.
- On Windows, files load 31% to 45% faster in projects with an isolated install. That's 50 to 100 ms per run on opencode's packages.

<!-- https://github.com/oven-sh/bun/commit/6bb5dc1956505dd1113455f58d4757c924f3ad54 -->

### `bun check -b` is up to 15x faster with many projects

With `bun check -b`, projects share parsed declaration files, module resolution caches and the list of emitted files. Each project used to keep its own copy, so check time grew much faster than the number of projects.

`bun check -b` on generated monorepos with `skipLibCheck: true`, best of 6 runs on a 16-core M4 Max:

| Monorepo                                                  | Bun v1.4.4 | Bun v1.4.3 | Faster |
| --------------------------------------------------------- | ---------: | ---------: | -----: |
| 1,000 independent projects, 5 files each                  |     0.53 s |     2.07 s |   3.9x |
| 4,000 independent projects, 5 files each                  |     0.87 s |    13.65 s |  15.7x |
| 800 projects with up to 40 references each, 20 files each |     1.26 s |     7.36 s |   5.8x |

In the last one, peak memory dropped from 1,987 MB to 872 MB.

<!-- https://github.com/oven-sh/bun/commit/788087c0a4e0c4e57dfe78156ba57a70e329d1b1 -->

## New in `bun install`

### `bun install` migrates Yarn Berry lockfiles

`bun install` converts a Yarn 2+ (Berry) `yarn.lock` to `bun.lock` and preserves every pinned version. Bun v1.4.3 only migrated Yarn v1 lockfiles, so switching a Berry project to Bun re-resolved every dependency.

```sh
$ bun install
bun install v1.4.4
[59.59ms] migrated lockfile from yarn.lock
```

- Workspaces, `npm:` aliases, `resolutions`, and git, tarball and `file:` dependencies are preserved.
- `patch:` dependencies become `patchedDependencies` in `package.json`.
- `catalog:` ranges are read from `.yarnrc.yml`. The catalogs are copied into `package.json`.

The migration is all-or-nothing. If Bun can't preserve everything `yarn.lock` pins, it prints the reason and falls back to resolving from `package.json`. For example:

- An `exec:` dependency.
- A registry set in `.yarnrc.yml` but not in `bunfig.toml` or `.npmrc`.

<!-- https://github.com/oven-sh/bun/commit/d1cb7db55a7b226c6ffbb876cfef635fe4959357 -->

### `bun install` retries with exponential backoff

`bun install` retries failed requests up to five times. In Bun v1.4.3, all five retries happened within 5 ms, so a brief registry outage would fail the install.

Now it waits 50 ms before the first retry and doubles the delay each time: 50, 100, 200, 400 and 800 ms, with ±20% jitter.

Installing one package from a misbehaving registry, and how long the command took:

| Registry           |    Bun v1.4.4 |   Bun v1.4.3 |
| ------------------ | ------------: | -----------: |
| One 503, then OK   |  works, 78 ms | works, 22 ms |
| 503 for 100 ms     | works, 222 ms |  fails, 8 ms |
| 503 or 429 for 1 s |  works, 1.6 s |  fails, 7 ms |
| 503 for 3 s        |  fails, 1.5 s | fails, 18 ms |
| Nothing listening  |  fails, 1.6 s | fails, 15 ms |

To tolerate outages longer than 1.5 seconds, increase the initial delay:

```sh
# waits 250 ms, 500 ms, 1 s, 2 s and 4 s
$ BUN_CONFIG_HTTP_RETRY_BACKOFF=250 bun install
```

- `BUN_CONFIG_HTTP_RETRY_BACKOFF=0` retries immediately, like before.
- `Retry-After` headers are respected, up to 60 seconds.
- 429 and 408 responses are retried.

<!-- https://github.com/oven-sh/bun/commit/8ba82c36d62d7fb42df72baf53c71b04db0f6d40 -->

### 133 `bun install` issues closed

We had 597 open package manager pull requests. 464 landed in full and 40 landed partially.

Every one of these PRs had tests that failed in the previous version of Bun and now pass. This nearly doubled bun install's test suite.

We also fuzzed it:

- We compared the old and new versions on 600 generated projects covering every kind of dependency, both linkers and both lockfile formats.
- We ran 7 commands against 90,000 mutated `bun.lockb` files without a crash or hang.
- We compared version selection against `npm-pick-manifest` on 200,000 generated cases. It differs in two: a `latest` tag that points to a missing version, and `^1.0.0-a.0`.

### Hoisted installs from a warm cache are up to 3.2x faster on Linux

The hoisted linker links packages on up to 8 threads on macOS and Linux. It helps most when the cache is warm and `node_modules` is missing or incomplete, like a CI job that restores Bun's cache.

{% image src="/images/hoisted-linker-1.4.4.svg" width="640" height="418" alt="Animation, in 50x slow motion, of bun install filling node_modules for a create-t3-app project. In Bun v1.4.4, folders like @trpc/server, drizzle-orm, next and zod are linked in parallel, and the install takes 91 ms. In Bun v1.4.3, the same folders are linked one at a time, and it takes 162 ms. Bun v1.4.4 is 1.8x faster." caption="create-t3-app, 102 packages, warm cache, Linux" /%}

Installing 1,000 packages with 30 files each on Linux, with everything already cached:

| `node_modules` | CPU cores |  After | Before |
| -------------- | --------: | -----: | -----: |
| Empty          |         8 | 120 ms | 381 ms |
| Empty          |         4 | 174 ms | 383 ms |
| Empty          |         2 | 162 ms | 222 ms |
| Half installed |         8 |  54 ms | 140 ms |
| Half installed |         4 |  65 ms | 174 ms |
| Half installed |         2 | 107 ms | 161 ms |

### Linux installs skip native packages built for the other libc

Packages like `sharp`, `@swc/core`, `lightningcss` and `rollup` publish one optional dependency per platform, and mark each Linux build with `"libc": ["glibc"]` or `"libc": ["musl"]`. Bun previously ignored that field, so every Linux install downloaded both builds. Now it only installs the one that matches your system, like npm and pnpm.

```sh
$ bun install --libc musl # for an Alpine image, from a glibc machine
$ bun install --libc '*'  # both builds, like before
```

The field is saved in `bun.lock`. Lockfiles written by older versions of Bun don't have it, so existing packages pick it up when they're updated or when you regenerate the lockfile.

### Peer dependencies resolve more like pnpm

When nothing in your project provides a package's peer dependency, Bun installs a version that satisfies the peer range. Before, the package got whatever version an unrelated package happened to install.

```json#package.json
{
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.20.0",
    "@remix-run/express": "2.17.1"
  }
}
```

`@remix-run/express` has a peer dependency on `express@^4.20.0`. The MCP SDK depends on `express@5`.

| Version    | `@remix-run/express` gets                                |
| ---------- | -------------------------------------------------------- |
| Bun v1.4.4 | `express@4`                                              |
| Bun v1.4.3 | `express@5`, with an `incorrect peer dependency` warning |

Installing `elysia` alongside `@jest/schemas` had the same problem: `elysia` got `@sinclair/typebox@0.27` and failed to load.

How `bun install` picks a peer dependency's version:

1. If your project, the workspace or any ancestor of the package depends on the peer, that version is used, even if it's out of range. With `react@17` in your app, every library it uses gets `react@17`.
2. Otherwise, every package with that peer dependency shares a single version that satisfies all of them. Bun prefers a version that's already installed, then the newest one in the registry.
3. If no single version satisfies all of them, Bun still avoids installing one copy per package, because packages like `react` and `graphql` break when two copies are loaded. Each package uses an installed version in its range if there is one, and otherwise shares the version the others use.

When an install has to resolve dependencies, `--verbose` prints every peer dependency that ended up out of range.

Versions already in `bun.lock` don't change. To re-resolve a peer, remove the package that needs it and add it back.

`bun update --latest` only needs to run once. In Bun v1.4.3, updating `react-dom@17` to `react-dom@19` changed its peer range, but a `react` that was only installed as its peer stayed on 17 until the second run.

### `bun pm fetch`

`bun pm fetch` downloads every dependency into the cache and nothing else. It doesn't create `node_modules`, run scripts or touch the lockfile.

In a Dockerfile, this keeps the downloads in a layer that's only rebuilt when `package.json` or `bun.lock` changes:

```dockerfile#Dockerfile
COPY package.json bun.lock ./
RUN bun pm fetch

COPY . .
RUN bun install --offline --frozen-lockfile
```

In a monorepo, the first step only needs the root `package.json` and the lockfile.

### `bun pm cache pack`, `unpack` and `prune`

`pack` writes the cache entries your lockfile needs into a single file, and `unpack` restores them on another machine.

```sh
# after an install
$ bun pm cache pack .ci/bun-cache.pack

# on another runner, before the install
$ bun pm cache unpack .ci/bun-cache.pack
$ bun install --frozen-lockfile
```

`unpack` only adds what's missing, and rejects a corrupted pack entirely.

`prune` removes old packages from the cache without clearing all of it:

```sh
$ bun pm cache prune             # older than 30 days
$ bun pm cache prune --max-age 7 # older than 7 days
$ bun pm cache prune --dry-run   # list what would go
```

### `bun publish --recursive` and `--filter`

Publish every package in a monorepo with one command. Dependencies are published before the packages that depend on them.

```sh
$ bun publish --recursive --quiet
skipped acme: private
skipped @acme/core@1.2.0: already on the registry
skipped @acme/docs: private

@acme/ui
Tag: latest
Access: default
Registry: https://registry.npmjs.org/

 + @acme/ui@1.2.0
```

- `--filter "@acme/*"` only publishes matching workspaces.
- Private packages and versions already on the registry are skipped, so it's safe to re-run after a failure.
- Every package is packed before the first one is published.

### `packageExtensions`

Some packages import a dependency they never declare, which fails with isolated installs. `packageExtensions` in the root `package.json` adds the missing entry without a fork or a patch.

```json#package.json
{
  "packageExtensions": {
    "legacy-plugin@2": {
      "dependencies": { "lodash": "^4.17.21" }
    },
    "react-redux@7": {
      "peerDependencies": { "react": "*" }
    }
  }
}
```

It uses the same format as pnpm and Yarn, and Bun copies the setting over when it migrates a `pnpm-lock.yaml` or `yarn.lock`.

### `link:` supports paths

`link:./dir` symlinks a directory into `node_modules`, like in Yarn and pnpm. The path is relative to the `package.json` that declares it. You don't need to run `bun link` first.

```json#package.json
{
  "dependencies": {
    "design-tokens": "link:./vendor/design-tokens",
    "shared": "link:../shared"
  }
}
```

`link:shared`, without a path, still refers to a package registered with `bun link`.

### `bun install` warns about deprecated packages

```sh
$ bun add rimraf@3
warn: deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
```

Deprecated direct dependencies each print one line. `--verbose` also lists transitive ones.

Like npm, Bun prefers non-deprecated versions when resolving a range, and `bun add pkg` skips a deprecated `latest`. Versions already in `bun.lock` don't change until you run `bun update`.

### `minimumReleaseAge` supports units and per-version exclusions

```toml#bunfig.toml
[install]
minimumReleaseAge = "3d" # also "90m", "12 hours", "2 weeks"
minimumReleaseAgeExcludes = ["@types/bun", "typescript@5.9.3"]
```

A plain number is still interpreted as seconds. `name@version` excludes a single release, like a security fix you need today, while the rest of that package's versions still have to wait.

Both are also available as flags: `--minimum-release-age 3d` and the new `--minimum-release-age-excludes typescript@5.9.3`.

### `--env-file` for the package manager, and new `bunx` flags

```sh
$ bun install --env-file ci.env
$ bunx --env-file ci.env prisma migrate deploy
$ bunx --cwd packages/api prisma migrate deploy
```

`bun install --env-file ci.env` used to try to install a package named `ci.env`.

- `--env-file <file>`, `--no-env-file` and `env = false` in `bunfig.toml` work with every package manager command.
- `bunx --cwd <dir>` runs the tool in another directory.
- `bunx --minimum-release-age <seconds>` applies the minimum release age to packages that `bunx` downloads.
- `bunx --trust` runs lifecycle scripts for the package and all of its dependencies.

### `bun add --dev` moves existing dependencies

`bun add -d`, `--optional` and `--peer` move a package that's already listed in another dependency group. They used to leave it where it was.

```sh
$ bun add -d typescript          # "dependencies" -> "devDependencies"
$ bun add --save-prod typescript # and back
```

### `bun install` reads the `.npmrc` that `actions/setup-node` writes

`actions/setup-node` writes its registry and token settings to a temporary `.npmrc` and points `NPM_CONFIG_USERCONFIG` at it. Bun reads that variable, so private registries work without copying the file.

```yaml#.github/workflows/ci.yml
- uses: actions/setup-node@v4
  with:
    registry-url: https://npm.pkg.github.com
- run: bun install
  env:
    NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### `bun install` understands npm, Yarn and pnpm flags, and rejects unknown ones

`bun install`, `bun ci`, `bun add`, `bun remove` and `bun update` translate flags from other package managers when Bun has an equivalent. Bun v1.4.3 ignored them, so `bun add --save-dev zod` saved to `dependencies` and `bun install --immutable` updated the lockfile.

| Flag                                                                        | Bun equivalent                             |
| --------------------------------------------------------------------------- | ------------------------------------------ |
| `--immutable`                                                               | `--frozen-lockfile`                        |
| `--package-lock-only`                                                       | `--lockfile-only`                          |
| `--save-dev`, `--save-exact`, `--save-optional`, `--save-peer`              | `--dev`, `--exact`, `--optional`, `--peer` |
| `--save-catalog`                                                            | `--catalog`                                |
| `--no-optional`, `--ignore-optional`                                        | `--omit=optional`                          |
| `--only=production`                                                         | `--omit=dev`                               |
| `--location=global`                                                         | `--global`                                 |
| `--shamefully-hoist`, `--node-linker=hoisted`, `--install-strategy=hoisted` | `--linker=hoisted`                         |
| `--node-linker=isolated`, `--install-strategy=linked`                       | `--linker=isolated`                        |

Flags with no Bun equivalent, like `--no-package-lock` or `--network-timeout 100000`, are ignored along with their value. Bun v1.4.3 only ignored the flag itself, so `bun install --network-timeout 100000` tried to add a package named `100000`.

Flags that none of these tools support are an error.

```sh
$ bun install --frozen-lockfil
error: bun install has no --frozen-lockfil flag.
note: Run 'bun install --help' for more information
```

Package manager commands and `bunx` used to ignore unknown flags, so this typo would silently run a normal install in CI.

### More in `bun install`

- `bun update --depth 0` only updates the dependencies in your `package.json`. Transitive dependencies keep the versions in `bun.lock`.
- `bun prune --check` prints what `bun prune` would remove without deleting anything, and exits with code 1 if there's anything to remove.
- `publishConfig.registry` and `publishConfig["@scope:registry"]` set the registry that `bun publish` publishes to.
- Like npm 11, `bun publish` refuses to publish a prerelease version like `2.0.0-beta.1` without `--tag` or `publishConfig.tag`, instead of tagging it `latest`.
- `workspace:*` can refer to the root package in `bun install`, `bun pm pack` and `bun publish`.
- `npm-shrinkwrap.json` is migrated, like `package-lock.json`.
- `bun.lock` records which packages have install scripts (`"hasInstallScript": true`), so installing from a committed lockfile reports blocked scripts. Older versions of Bun ignore the new key and drop it the next time they save the lockfile.
- When `bun install --frozen-lockfile` or `bun ci` fails because `package.json` changed, the error names the dependency, such as `is-odd was added` or `is-number changed from ^6.0.0 to ^7.0.0`.
- `--dry-run` works with `bun link`, `bun unlink`, `bun pm version`, `bun pm cache rm`, `bun pm trust`, `bun pm pkg` and `bun pm migrate`. Before, they accepted the flag but still wrote changes.
- `bun pm pkg set` and `delete` support arrays (`keywords[]=cli`, `contributors[0].name=Jane`) and bracketed keys (`exports[./package.json]`).
- `bun pm view` and `bun info` work outside of a project, accept multiple fields at once, and support npm-style paths like `maintainers[0].name`.
- `bun info`, `bun pm view`, `bun pm whoami` and `bun audit` retry failed requests like `bun install` does, instead of failing on a single 503 or dropped connection.
- `bun pm ls` accepts package names (`bun pm ls lodash`), `--production`, `--omit` and `--depth=0`.
- `bun pm version` sets `$npm_old_version`, `$npm_new_version`, `$npm_command` and `$npm_package_version` for the `preversion`, `version` and `postversion` scripts.
- Lifecycle scripts of dependencies get their own `npm_package_name`, `npm_package_version`, `npm_package_json` and `npm_package_config_*`, plus `npm_lifecycle_event`, `npm_lifecycle_script` and `npm_config_local_prefix`.
- `"gypfile": false` in a package disables the implicit `node-gyp rebuild`.
- `bun create <template>` supports `--ignore-scripts`. It also skips the template's preinstall and postinstall tasks when `ignoreScripts = true` is set in `bunfig.toml` or `ignore-scripts=true` in `.npmrc`.
- `bunx -p pkg cmd` installs `pkg`, then runs `cmd` with `pkg`'s binaries first in `PATH`, like npx.
- Auto-install reads registries and credentials from `.npmrc`.
- Auto-install uses the versions pinned in `bun.lock` or `bun.lockb`.
- Auto-install reads the project's `package.json` the same way `bun install` does: `devDependencies`, `peerDependencies`, `overrides`, `resolutions` and `catalogs` are applied, and `workspace:`, `file:` and `link:` dependencies are loaded from disk.
- Security scanners receive every resolved dependency, including git, GitHub, tarball, `file:` and `link:` ones.
- `Cache-Control: no-cache`, `no-store` and a `max-age` under 5 minutes on registry metadata are respected.
- `--use-system-ca` works with `bun install` and the other package manager commands. Only `NODE_USE_SYSTEM_CA=1` worked before.
- `BUN_INSTALL`, `XDG_CACHE_HOME`, `BUN_INSTALL_CACHE_DIR`, `BUN_INSTALL_GLOBAL_DIR`, `BUN_INSTALL_BIN` and `NPM_CONFIG_USERCONFIG` accept a leading `~/`.
- `bun audit fix` reports and rewrites an `overrides` rule that pins a package to a vulnerable version.
- `bun pm migrate` on a Yarn v1 lockfile with 20,000 entries takes 0.27 s, down from 24 s.
- `bun install` in a monorepo with 2,100 workspaces peaks at 35 MB of memory, down from 91 MB.
- Applying a patch no longer takes quadratic time. `bun patch --commit` with 40,000 changed lines in a 100,000-line file takes 0.09 s instead of 1.45 s.
- With `globalStore = true`, a no-op `bun install` no longer deletes and re-creates every `node_modules/.bun/<pkg>` link. The store rebuilds its entries once after this upgrade, and `bun pm cache rm` frees the old ones.

Thanks to @nikelborm for the contribution!

## New in `Bun.SQL` and `bun:sqlite`

### `Bun.SQL` uses up to 48% less CPU per query

We reworked `Bun.SQL`, `bun:sqlite` and `node:sqlite` in a single change that replaces 149 open pull requests and fixes 51 issues.

Every query allocates less memory and executes fewer instructions on the client.

Client-side CPU usage with PostgreSQL, measured in user-space instructions on Linux x64:

| Operation                            | Change |
| ------------------------------------ | -----: |
| Point `SELECT`                       |   -48% |
| 100-row `SELECT`                     |   -30% |
| `INSERT`                             |   -21% |
| Bulk insert, 1,000 rows x 10 columns |   -38% |

The same bulk insert uses 53% fewer instructions on MySQL.

Garbage-collected objects allocated per `` await sql`select 1` ``:

| Adapter    | After | Before |
| ---------- | ----: | -----: |
| PostgreSQL |    28 |     84 |
| MySQL      |    27 |     76 |
| SQLite     |    33 |     52 |

### `query.cancel()` cancels a running PostgreSQL query

`cancel()` was already in the types and docs, but it didn't do anything. Now Bun asks the server to cancel the query, and the promise rejects with the server's error.

```ts
const query = sql`SELECT pg_sleep(30)`.execute();
setTimeout(() => query.cancel(), 100);

await query; // rejects: canceling statement due to user request
```

A query that hasn't been sent yet is never sent, and rejects with `ERR_POSTGRES_QUERY_CANCELLED`, `ERR_MYSQL_QUERY_CANCELLED` or `ERR_SQLITE_QUERY_CANCELLED`. On MySQL and MariaDB, a query that's already running on the server still runs to completion.

### `COPY` streams for PostgreSQL

`query.writable()` writes to `COPY ... FROM STDIN`, and `query.readable()` reads from `COPY ... TO STDOUT`. Both are web streams with backpressure, so memory usage stays flat no matter how large the table is.

```ts
// Load a CSV file into a table
await Bun.file("users.csv")
  .stream()
  .pipeTo(sql`COPY users (id, name) FROM STDIN (FORMAT csv)`.writable());

// Stream a table as an HTTP response
new Response(sql`COPY users TO STDOUT (FORMAT csv, HEADER)`.readable());
```

The streams carry raw bytes in whichever format the statement specifies: `text`, `csv` or `binary`. A 1,000 MiB `COPY` streams at 255 MiB/s with a peak RSS of 59 MiB.

Thanks to @gohryt for the contribution!

### Column name transforms in `Bun.SQL`

`transform: SQL.camel` converts column names in both directions: `first_name` is read as `firstName`, and `firstName` is written as `first_name`. It works with every adapter.

```ts
import { SQL } from "bun";

const sql = new SQL({ transform: SQL.camel });

await sql`INSERT INTO users ${sql({ firstName: "Alice" })}`;
// INSERT INTO users ("first_name") VALUES ($1)

const [user] = await sql`SELECT first_name FROM users`;
user.firstName; // "Alice"
```

- `SQL.camel`, `SQL.pascal` and `SQL.kebab` convert in both directions.
- `SQL.toCamel` only converts results, and `SQL.fromCamel` only converts names passed to `sql()` helpers. `pascal` and `kebab` have the same variants.
- `{ column: { from, to } }` accepts custom functions.

The names match postgres.js.

Thanks to @pjb3 for the contribution!

### Pass JavaScript arrays to PostgreSQL array columns

You can pass a JavaScript array or an `Int32Array` anywhere PostgreSQL expects a built-in array type, including arrays with `null`s and nested arrays. Both of these queries used to fail.

```ts
await sql`SELECT * FROM users WHERE id = ANY(${[1, 2, 3]})`;

// tags text[]
await sql`INSERT INTO posts ${sql({ title: "Hello", tags: ["bun", "sql"] })}`;
```

35 more built-in array types, such as `uuid[]`, `int4range[]` and `tsvector[]`, are returned as arrays instead of the string `"{...}"`.

For an array of a user-defined type such as an enum, use `sql.array(values, "mood")`.

### `result.columns` and `result.statement`

On PostgreSQL, MySQL and MariaDB, results include the column metadata reported by the server. Use `type` to distinguish columns that produce the same JavaScript value, such as `jsonb` and `text[]`.

```ts
const result = await sql`SELECT id, tags FROM posts WHERE id = ${1}`;

result.columns;
// [
//   { name: "id", type: 23, table: 16388, number: 1 }, // int4
//   { name: "tags", type: 1009, table: 16388, number: 2 }, // text[]
// ]

result.statement.string; // the text Bun sent, with $1 for the parameter
result.statement.types; // [23]
```

MySQL and MariaDB columns are `{ name, type, table, length, flags }`. Both properties are `null` on SQLite.

### `sql.json()`

`sql.json(value)` stores any value as JSON and works with every adapter. Use it for numbers, booleans, strings or `null`.

```ts
await sql`INSERT INTO settings (data) VALUES (${sql.json(42)})`; // 42
await sql`INSERT INTO settings (data) VALUES (${sql.json("dark")})`; // "dark"
await sql`INSERT INTO settings (data) VALUES (${sql.json(null)})`; // JSON null
```

A bare `null` is still SQL `NULL`.

### `bun --hot` reuses database connections

Every save under `bun --hot` used to open up to `max` new PostgreSQL or MySQL connections and leave the old ones open. Now a `new SQL()` with the same settings reuses the connections from before the reload.

With `max: 2`, five reloads used to leave 12 sessions open on the server. Now there are 2.

Session state like `SET` and temporary tables is preserved. `onconnect` runs again for each connection that's reused.

Thanks to @eriklangille for the contribution!

### New `Bun.SQL` connection options

```ts
const sql = new SQL("postgres://app@db.example.com/app?connect_timeout=5", {
  channelBinding: "require",
  tls: {
    ca: Bun.file("./ca.pem"),
    checkServerIdentity: (hostname, cert) =>
      cert.fingerprint256 === pin ? undefined : new Error("pin mismatch"),
  },
});
```

- `channelBinding`: `"disable"` (the default), `"prefer"` or `"require"`. It binds PostgreSQL SCRAM authentication to the server's TLS certificate (SCRAM-SHA-256-PLUS). Also available as `?channel_binding=` and `PGCHANNELBINDING`.
- `tls.checkServerIdentity(hostname, cert)`: replaces the built-in hostname check on PostgreSQL, MySQL and MariaDB, like in `node:tls`. Passing it upgrades any SSL mode below `verify-ca` to `verify-full`.
- `foundRows: true`: on MySQL and MariaDB, `affectedRows` counts the rows an `UPDATE` matched instead of the rows it changed, like `mysql2`. Also available as `?foundRows=true`.
- `?connect_timeout=`, `?idle_timeout=`, `?max_lifetime=` and `?max=` in PostgreSQL and MySQL URLs.
- libpq's URL parameters on PostgreSQL: `host`, `port`, `user`, `password`, `dbname`, `sslrootcert`, `sslcert`, `sslkey` and `sslpassword`. Bun used to forward them to the server, which refused the connection.
- `PGAPPNAME`, `PGOPTIONS` and `PGCONNECT_TIMEOUT` environment variables.

### User-defined functions in `bun:sqlite`

`db.function()` defines a SQL function that calls a JavaScript function.

```ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
db.function("add2", (a, b) => a + b);

db.query("SELECT add2(12, 4) AS sum").get(); // { sum: 16 }
```

`db.aggregate()` defines an aggregate function such as `sum()`.

```ts
db.aggregate("add_all", {
  start: 0,
  step: (total, value) => total + value,
});

db.query("SELECT add_all(dollars) AS total FROM expenses").get();
```

- Both accept the `varargs`, `deterministic`, `directOnly` and `safeIntegers` options.
- `inverse` lets an aggregate be used as a window function.
- Errors thrown by a function propagate to the `get()`, `all()` or `run()` call that ran the query.

The signatures match better-sqlite3.

Thanks to @nikeee and @jdjkelly for the contribution!

### Update hooks in `bun:sqlite`

Set `db.onInsert`, `db.onUpdate` or `db.onDelete` to a function, and SQLite calls it for each row that this `Database` inserts, updates or deletes.

```ts
db.onInsert = (db, tableName, rowid) => {
  console.log(`inserted ${tableName} ${rowid}`);
};

db.run("INSERT INTO cats (name) VALUES ('Salem'), ('Keanu')");
// inserted cats 1
// inserted cats 2
```

Set it to `null` to remove the hook.

Also new:

- `stmt.reader` is `true` if the statement returns rows.
- `stmt.readonly` is `true` if the statement doesn't directly modify the database.
- `db[Symbol.for("sqlite-type")]` is `"bun:sqlite"`. Libraries can use it to distinguish a `Database` from a `node:sqlite` `DatabaseSync`.

Thanks to @jecquas and @himself65 for the contribution!

## New in `WebSocket` and `ws`

### `ws` behaves like the npm package on Node.js

`import "ws"` in Bun loads a built-in implementation backed by Bun's native WebSocket client and server. We rewrote it as a port of `ws` 8.22.0. Framing, masking, UTF-8 validation and compression are still native.

| Compared to npm `ws` on Node.js                    | Bun v1.4.4 | Before |
| -------------------------------------------------- | ---------: | -----: |
| `ws`'s own test suite, 271 tests                   |        234 |     94 |
| 212 client scripts, byte-for-byte identical output |        179 |     18 |
| 159 server scripts, byte-for-byte identical output |        150 |     67 |

The remaining failures depend on `ws` internals that the native implementation doesn't have, like `ws._receiver` and `generateMask`.

```ts
import { WebSocket, createWebSocketStream } from "ws";

const ws = new WebSocket("wss://example.com/logs", {
  handshakeTimeout: 5_000,
  followRedirects: true,
  maxPayload: 1024 * 1024,
});

createWebSocketStream(ws, { encoding: "utf8" }).pipe(process.stdout);
```

- `createWebSocketStream()` is implemented, with backpressure. It used to throw "Not supported yet in Bun".
- `agent`, `createConnection` and `lookup` are respected, so SOCKS proxy agents work. A custom `agent` used to be ignored, and the client connected directly.
- `handshakeTimeout`, `closeTimeout`, `followRedirects`, `maxRedirects`, `origin`, `auth`, `protocolVersion`, `autoPong` and `skipUTF8Validation` are implemented instead of silently ignored.
- `maxPayload` is enforced on clients and servers. socket.io's `maxHttpBufferSize` depends on it.
- `WebSocketServer({ perMessageDeflate })` negotiates compression.
- `WebSocketServer` accepts any `Duplex`. This includes `http2.createSecureServer({ allowHTTP1: true })` (`vite --https`), `server.emit("connection", socket)` (socket.io sticky sessions) and fastify's `injectWS`.
- Server sockets have `pause()`, `resume()` and `isPaused`. They're `instanceof WebSocket`, and `ws._socket` is the `node:http` socket.

### `ws` events match Node.js

```ts
ws.on("error", (error) => {
  error instanceof Error; // true, was an ErrorEvent
  error.code; // "ECONNREFUSED"
});

ws.on("close", (code, reason) => {
  reason; // a Buffer, was a string
});
```

- An `'error'` event without a listener is an uncaught exception, like in Node.js. Add an `'error'` listener to every client and every server socket.
- `'close'` fires when the connection closes, with the peer's code and reason. `close()` without a code reports 1005, and a failed connection reports 1006.
- Error messages match the npm package's, like `Unexpected server response: 403`.
- `send()` throws while the socket is `CONNECTING`.
- `ws` servers have no idle timeout, no automatic pings and no backpressure limit. They used to inherit `Bun.serve`'s defaults: a 120 second idle timeout, a ping at 104 seconds and a 16 MiB limit.
- The default `maxPayload` is 100 MiB, up from 16 MiB.

### `WebSocket` waits for the closing handshake

The `close` event fires once the connection has closed, like in browsers and Node.js. It used to fire right after `close()` with your own code and `wasClean: true`, regardless of what the server did.

```ts
const ws = new WebSocket("wss://example.com");
ws.onopen = () => ws.close();

ws.onclose = (event) => {
  event.code; // the server's code, 1005 if its Close frame had none
  event.wasClean; // false if the server never sent a Close frame
};
```

- `close()` without a code sends an empty Close frame. Both ends report 1005 instead of 1000, including the `close(ws, code)` handler in `Bun.serve`.
- A script that ends with `ws.close()` takes one extra round trip to exit. If the server doesn't respond within 30 seconds, `close` fires with 1006.
- `BUN_CONFIG_WS_CLOSE_TIMEOUT` changes the 30 second timeout. `ws.terminate()` closes immediately.

### Failed `WebSocket` connections fire `error`, then `close` with 1006

A bad handshake, a TLS or proxy error, a protocol error, an oversized message and invalid UTF-8 are all reported the same way, like in browsers and Node.js. `event.error.code` is new.

```ts
const ws = new WebSocket("ws://localhost:1");

ws.onerror = (event) => {
  event.error.code; // "ECONNREFUSED"
};

ws.onclose = (event) => {
  event.code; // 1006
  event.reason; // ""
  event.wasClean; // false
};
```

Before, `close` fired with 1002, 1007, 1009, 1011 or 1015 and an English reason, sometimes with `wasClean: true`, and often no `error` event fired.

- `event.message` has the description that used to be in `reason`, with more detail: `Expected 101 status code: 503`, `TLS handshake failed: self signed certificate`.
- `event.error.code` is a Node.js-style code such as `ENOTFOUND`, `DEPTH_ZERO_SELF_SIGNED_CERT`, `ERR_TLS_CERT_ALTNAME_INVALID` or `WS_ERR_INVALID_UTF8`.
- Losing the TCP connection after `open` isn't treated as a failure. Only `close` fires, with 1006.

### `ws.pause()` and `ws.resume()` in `Bun.serve`

Use these to slow down a client that sends faster than you can process. While a socket is paused, Bun stops reading from it, so the client sees TCP backpressure.

```ts
Bun.serve({
  fetch(req, server) {}, // upgrade logic
  websocket: {
    message(ws, message) {
      if (!file.write(message)) {
        ws.pause();
        file.once("drain", () => ws.resume());
      }
    },
  },
});
```

`ws.isPaused` tells you whether the socket is paused. The idle timeout is suspended while a socket is paused.

### `websocket.error` in `Bun.serve` receives the socket

When a `websocket` handler throws, Bun calls `error` with the error and the socket it came from. It only received the error before, and it was missing from the types.

```ts
Bun.serve({
  fetch(req, server) {}, // upgrade logic
  websocket: {
    message(ws, message) {
      throw new Error("oops");
    },
    error(error, ws) {
      ws.close(1011);
    },
  },
});
```

Every handler is optional, including `message`.

### `proxy.tls`: separate TLS options for an HTTPS proxy

With an `https://` proxy, `tls` applies to both the proxy and the server. Pass `proxy.tls` to configure them separately. The proxy then only uses `proxy.tls`, and the server only uses `tls`.

```ts
const ws = new WebSocket("wss://example.com", {
  proxy: {
    url: "https://proxy.internal:8443",
    tls: { ca: Bun.file("./proxy-ca.pem") },
  },
});
```

Thanks to @steipete, @hughescr, @davidhevn and @deepshekhardas for the contributions!

## New in the runtime

### JavaScriptCore upgrade

This release upgrades JavaScriptCore with 1,013 upstream WebKit commits.

Measured on an Apple silicon Mac, best of several runs:

| Operation                                                       | Bun v1.4.4 | Bun v1.4.3 | Faster |
| --------------------------------------------------------------- | ---------: | ---------: | -----: |
| `n.toLocaleString("en-US")`, 100,000 calls                      |      25 ms |   1,341 ms |    54x |
| Loading an array literal of 1,000,000 integers                  |      11 ms |      45 ms |   4.2x |
| `for (const ch of str)`, 1,000,000 characters, some non-Latin-1 |     1.2 ms |     3.2 ms |   2.7x |
| `new Array(n)` with `n` under 4, 10,000,000 calls               |      31 ms |      61 ms |   2.0x |
| `try { throw "x" } catch {}`, 1,000,000 times                   |      37 ms |      72 ms |   2.0x |
| `try { throw new Error("x") } catch {}`, 1,000,000 times        |     128 ms |     174 ms |   1.4x |
| `new WeakMap()` and one `set()`, 1,000,000 times                |     7.7 ms |     9.9 ms |   1.3x |

`toLocaleString()` with a locale and no options reuses a cached `Intl.NumberFormat`, which makes it as fast as creating the formatter yourself and calling `format()`. This applies to numbers and BigInts.

- `using` is allowed in a function nested inside a `switch` case. It was a `SyntaxError`.
- `+1n * 2n` throws a `TypeError`. It used to return `2n`.
- `+a * b()` calls `a.valueOf()` before `b()`.
- `for...in` over a function visits its indexed properties, like `fn[0]`.
- JIT-optimized `(x + y + 0.5) | 0` returns the right result when `x + y` overflows 32 bits.
- JIT-optimized code throws a `RangeError` for `new Uint8Array(-1)` even if the array is never used.
- In sloppy mode, `label: function f() {}` is hoisted like an unlabeled function declaration.
- WebAssembly's baseline compiler keeps a function's most-used locals in registers.

<!-- https://github.com/oven-sh/bun/commit/f2405c382d43b89d91a2e7c90d9c5f53505a1e71 -->

### `Bun.Glob` supports extglob and brace ranges

```ts
const source = new Bun.Glob("**/!(*.test).ts");
source.match("src/cart.ts"); // true
source.match("src/cart.test.ts"); // false

new Bun.Glob("src/@(app|lib)/**/*.ts").match("src/app/a/b.ts"); // true
new Bun.Glob("shard-{1..3}.json").match("shard-2.json"); // true
```

| Pattern                                     | Matches                                                       |
| ------------------------------------------- | ------------------------------------------------------------- |
| `?(a\|b)`                                   | Zero or one of the patterns                                   |
| `*(a\|b)`                                   | Zero or more of the patterns                                  |
| `+(a\|b)`                                   | One or more of the patterns                                   |
| `@(a\|b)`                                   | Exactly one of the patterns                                   |
| `!(a\|b)`                                   | Anything in a path segment except the patterns                |
| `{1..3}`, `{01..10}`, `{a..e}`, `{1..9..2}` | A number or letter in the range. The third value is the step. |

The new syntax works everywhere Bun uses globs, including `--filter` and `workspaces`, `files` and `sideEffects` in `package.json`.

```json#package.json
{
  "workspaces": ["packages/!(internal)"]
}
```

In Bun v1.4.3, this failed with `error: Workspace not found "packages/!(internal)"`.

A `(` that isn't preceded by `?`, `*`, `+`, `@` or `!` is still a literal character, so Next.js route groups like `app/(marketing)/**` still match.

These patterns used to be matched literally. They now have a different meaning, consistent with picomatch, minimatch and bash's `extglob`:

| Pattern     | Bun v1.4.3                | Bun v1.4.4                                                 |
| ----------- | ------------------------- | ---------------------------------------------------------- |
| `*(1).txt`  | Matched `copy(1).txt`     | Zero or more `1`, then `.txt`. Write `*[(]1).txt` instead. |
| `!(g)/a.ts` | Everything but `(g)/a.ts` | `a.ts` in any folder but `g`                               |
| `{1..3}`    | The text `1..3`           | `1`, `2` or `3`                                            |

This only affects patterns, not paths. A file with those characters in its name still matches `**/*.ts`.

Bun Shell is unaffected: quoted text and `${}` values are still treated literally.

<!-- https://github.com/oven-sh/bun/commit/bfa1209e35663056248406661f1b90851e5c72fa -->

### Up to 2.8x faster `Headers` iteration

`for...of`, `entries()`, `keys()`, `values()`, `forEach()` and `Object.fromEntries(headers)` are 1.7x to 2.8x faster. Adapters that copy a `Response`'s headers into `node:http`, like `@hono/node-server`, do this on every request.

```ts
for (const [name, value] of response.headers) {
  res.setHeader(name, value);
}
```

Time to iterate over one `Headers` object on an M4 Max, best of 7 runs:

| Operation                                 |  v1.4.4 |  v1.4.3 | Faster |
| ----------------------------------------- | ------: | ------: | -----: |
| `for...of`, 3 headers                     |   98 ns |  277 ns |   2.8x |
| `for...of`, 14 headers                    |  627 ns | 1.41 µs |   2.2x |
| `for...of`, 14 custom `X-` headers        | 1.24 µs | 2.08 µs |   1.7x |
| `headers.forEach()`, 14 headers           |  505 ns | 1.10 µs |   2.2x |
| `[...headers.keys()]`, 14 headers         |  261 ns |  688 ns |   2.6x |
| `[...headers.values()]`, 14 headers       |  295 ns |  680 ns |   2.3x |
| `Object.fromEntries(headers)`, 14 headers |  579 ns | 1.46 µs |   2.5x |

<!-- https://github.com/oven-sh/bun/commit/408c9374b266b45fe6b8260e09be73aa82a957cd -->

### Up to 2.6x faster `response.body` streams for in-memory bodies

Streaming a body that's already in memory (`new Response("...").body`, `request.body`, `blob.stream()`) is 1.5x to 2.6x faster. Reading a 100-byte body allocates 7 KB instead of 270 KB.

```ts
for await (const chunk of new Response(html).body) {
  res.write(chunk);
}
```

Time to read an entire `new Response(string).body` with `reader.read()`, on Linux x64:

| Body  |   After |   Before |
| ----- | ------: | -------: |
| 100 B | 1.41 µs |  3.66 µs |
| 4 KB  | 1.77 µs |  4.15 µs |
| 16 KB | 2.93 µs |  5.54 µs |
| 64 KB | 9.11 µs | 14.33 µs |
| 1 MB  |  176 µs |   272 µs |

Adapters that copy a `Response` into a `node:http` response read the body this way. A Hono app on `@hono/node-server` handled a batch of 24 requests in 16.0 ms, down from 24.0 ms.

<!-- https://github.com/oven-sh/bun/commit/5ee74f02a2586059ab4d496a0f35d3a788e5e807 -->

### `--disallow-code-generation-from-strings=strict` only runs code that ships with your program

`=strict` used to block a fixed list of APIs. Now JavaScriptCore refuses to run any code that isn't a Bun built-in or one of your program's own files, regardless of which API it came from.

For a `bun build --compile` executable, only the code inside the executable counts as your program. JavaScript and TypeScript files on disk no longer run, including files the program just wrote:

```ts
const path = "/tmp/plugin.js";
await Bun.write(path, "console.log('hi')");
await import(path);
// EvalError: Code generation from strings disallowed for this context
```

Without `--compile`, files on disk count as your program, so that import still runs.

`=strict` also blocks:

- Plugins that return `contents`, for any loader. Return an `exports` object instead.
- Macros.
- In a compiled executable, `--preload` in `BUN_OPTIONS` and `.txt` or `.md` imports from disk.

`.json`, `.toml` and `.yaml` files still load from disk. `import()` of a `data:` URL containing JSON is no longer blocked.

<!-- https://github.com/oven-sh/bun/commit/5f5799ed16568240de19fef364bd4c9bf22ca1c7 -->

## New in the dev server

### Redesigned error overlay in the dev server

The error overlay for `Bun.serve` with HTML imports has a new design, and similar errors in the same file are grouped into one card. Three missing fonts in a stylesheet used to show up as three separate blocks.

```txt
BUILD FAILED
4 errors in 2 files

app.ts                                         1 ERROR
  ERROR  Could not resolve: "./render"
      1  import { render } from "./render";

styles.css                                    3 ERRORS
  ERROR  Could not resolve                          ×3
      3  src: url("./fonts/inter-regular.woff2");
      8  src: url("./fonts/inter-medium.woff2");
     13  src: url("./fonts/inter-bold.woff2");
```

- The overlay scrolls. Errors past the bottom of the window used to be unreachable.
- Build errors are shown once, not twice, when a file is bundled for both the server and the browser, like a stylesheet imported by a page.
- There's a light theme that follows your system setting.
- Runtime errors use the same design, with the error's name as the title.

<!-- https://github.com/oven-sh/bun/commit/91e7e3b140eaef348b7bc33d564dcb67b7de4ddd -->

## Other improvements

- Improved: `req.headers` and `res.setHeader()` have less overhead in `node:http` servers. 256 requests through Express 5 take 9.45 ms, down from 9.91 ms (Linux x64).

<!-- https://github.com/oven-sh/bun/commit/61009bd2f332822cbf29e69829bbe9c33e3c2150 -->

- Improved: `Bun.JSON5.parse()` is up to 1.6x faster. A 126 KB `bun.lock` parses in 0.42 ms, down from 0.67 ms (M4 Max).

<!-- https://github.com/oven-sh/bun/commit/788087c0a4e0c4e57dfe78156ba57a70e329d1b1 -->

- Improved: `Bun.Glob.match()` is 1.6x to 5x faster. Matching `**/*.{ts,tsx}` against `src/components/button/index.tsx` takes 32 ns, down from 158 ns (M4 Max).

<!-- https://github.com/oven-sh/bun/commit/bfa1209e35663056248406661f1b90851e5c72fa -->

- Improved: root certificates are updated to NSS 3.131.1.

- Improved: native addons on Linux and macOS can call 47 more libuv functions, including `uv_async_*`, `uv_timer_*`, `uv_idle_*`, `uv_prepare_*`, `uv_check_*`, `uv_queue_work`, `uv_default_loop` and `uv_cwd`. Bun used to exit with "unsupported uv function".
- Improved: echoing a 1 MB non-ASCII WebSocket text message in `Bun.serve` takes 72% fewer CPU instructions.
- Improved: `Bun.markdown.html()` on large non-ASCII input takes 33% fewer CPU instructions.

- Improved: `Bun.hash()` and its algorithms are 1.2x to 2.3x faster on short strings. `Bun.hash.crc32()` on a 16-character ASCII string takes 8 ns, down from 15 ns (M4 Max).
- Improved: `Bun.CryptoHasher`, `Bun.SHA256` and the other hashers are 1.2x to 1.7x faster on small inputs. Hashing 16 bytes to hex with `Bun.SHA256.hash()` takes 77 ns, down from 107 ns (M4 Max).

- Improved: `new TextDecoder()` with an options object, like `new TextDecoder("utf-8", { fatal: true })`, is 2x faster.

<!-- https://github.com/oven-sh/bun/commit/3880151f0abacaf7dce9d6d0ab1c7be295d14bb4 -->

- Fixed: a regression in Bun v1.4.3 where runs that write the compile cache (`NODE_COMPILE_CACHE` or `module.enableCompileCache()`) took 2x longer to exit (580 ms instead of 300 ms for a 3.4 MB module).

<!-- https://github.com/oven-sh/bun/commit/0663016fd8e3f824b1ecf6298ca6b4595d40f8d4 -->

- Improved: Bun's FreeBSD binaries are built against FreeBSD 14.4. The previous target, 14.3, has reached end of life.

<!-- https://github.com/oven-sh/bun/commit/b7efd52c3b36c17a29e7485fd8a45962ac2af701 -->

## Security

This release includes security fixes for TLS, WebSocket, the database clients and the package manager. These protocols, APIs and commands are affected:

TLS, HTTP/1.1, HTTP/2, HTTP/3, WebSocket, Brotli, `Bun.serve`, `Bun.listen`, `Bun.connect`, `Bun.SQL`, `Bun.RedisClient`, `Bun.password`, `Bun.Image`, `Bun.Archive`, `node:tls`, `node:https`, `node:http`, `node:http2`, `node:net`, `node:quic`, `node:crypto`, `node:vm`, `node:v8`, `ws`, `fetch`, `TextEncoder`, `bun install`, `bunx`, `bun patch`, `bun publish`, `bun pm pack`, `bun pm trust`, `bun audit` and `bun create`.

## Bugfixes

### Node.js compatibility improvements

- Fixed: various stability issues in `node:http`, `node:http2`, `node:quic`, `node:dgram`, `node:dns`, `node:crypto`, `node:vm`, `node:stream`, `node:zlib`, `node:util`, `node:diagnostics_channel`, `node:worker_threads` and native addons.

#### `node:http`

- Fixed: in a `node:http` server, `res.end()` threw `ERR_INVALID_ARG_TYPE` when `res.statusCode` was a numeric string like `"404"` or a fraction like `200.5`. It now sends `404` and `200`, like Node.js.
- Fixed: in a `node:http` server, `res.end()` sent `200` when `res.statusCode` was `NaN` or `undefined`. It now throws `ERR_HTTP_INVALID_STATUS_CODE`, like Node.js.
- Fixed: in a `node:http` server, after `res.end({})` threw because of the invalid chunk, `res.headersSent` was `true` even though nothing had been sent, so `res.setHeader()` threw `ERR_HTTP_HEADERS_SENT`.

<!-- https://github.com/oven-sh/bun/commit/59c1ac474cdbafe9d77b395bd2712b767b617767 -->

#### `node:http2`

- Fixed: `pushStream()` in `node:http2` threw `ERR_HTTP2_OUT_OF_STREAMS` instead of passing it to the callback.
- Fixed: with `http2.createSecureServer()` and `server.emit("connection", socket)`, `session.socket` wasn't a `TLSSocket` and `'connection'` listeners didn't fire.

#### `node:fs`

- Fixed: on Linux and macOS, every `fs.realpath()` variant treated `\` as a path separator, so a file named `a\b` threw `ENOENT`, or resolved to `a/b` if that existed.
- Fixed: on Linux and macOS, `fs.realpath.native()`, `fs.realpathSync.native()` and `fs.promises.realpath()` collapsed `..` before following symlinks, so `link/../y` resolved next to the link instead of its target, and `missing/..` didn't throw.
- Fixed: on Linux and macOS, `fs.realpath()` and `fs.realpathSync()` threw `ENOTDIR` for a regular file's path with a trailing slash, like `file/`. Like Node.js, they now return the file's path.
- Fixed: on Windows, `fs.promises.realpath("C:\\")` returned `C:` instead of `C:\`.

<!-- https://github.com/oven-sh/bun/commit/c0e1716f17386fb1683320ea1c7ac78c33c48a3a -->

#### `node:vm`

- Fixed: `vm.createContext(vm.constants.DONT_CONTEXTIFY)` returned a wrapper instead of the context's global. Top-level `var` and `function` declarations were lost, and Vitest with jsdom 30.1 failed to start.
- Fixed: in a `vm.createContext(vm.constants.DONT_CONTEXTIFY)` context, scripts threw a `ReferenceError` for names inherited from the global's prototype chain, including through a `Proxy`. jsdom 30 with `runScripts` relies on this.

#### `node:stream`

- Fixed: `Readable.fromWeb(stream).destroy()` emitted `'error'` when the web stream's `cancel()` rejected. Node.js doesn't.

#### `node:crypto`

- Fixed: an X25519 `CryptoKey` couldn't be cloned with `structuredClone()` or sent with `postMessage()`.
- Fixed: `write()` on a `node:crypto` `Hash` or `Hmac` threw `ERR_CRYPTO_HASH_FINALIZED` after `digest()`. The chunk is now dropped, like in Node.js.

#### `node:util`

- Fixed: calling `new` on a native function that isn't a constructor, like `util.types.isDate`, returned the result of calling it. It now throws a `TypeError`, like Node.js. These functions were also printed as `[class isDate]`.
- Fixed: `util.types.isProxy(globalThis)` returned `true`.

#### `process`

- Fixed: registering the same function twice with `process.on()` only added it once. It's now added and called twice, like in Node.js.
- Fixed: after `import * as process from "node:process"`, `process.on()` didn't add the listener to `process`.

#### `node:diagnostics_channel`

- Fixed: in `node:diagnostics_channel`, the next subscriber was skipped when a subscriber unsubscribed itself during `publish()`.
- Fixed: in `node:diagnostics_channel`, a subscriber added during `publish()` was called during that same `publish()`.
- Fixed: `channel.unsubscribe()` threw a `TypeError` when `channel._subscribers` held a frozen or sealed array.

<!-- https://github.com/oven-sh/bun/commit/a407eae88ac76d2f54068b00a9f79c7b9ccc0f0f -->

#### `node:worker_threads`

- Fixed: in a `Worker` with `env: SHARE_ENV`, `join()`, `slice()` and `concat()` treated holes as empty in an array with `process.env` in its prototype chain. `arr[1]` already read `process.env[1]`.

<!-- https://github.com/oven-sh/bun/commit/dbe1a63c86436c79652b24f919e7d3393a82d411 -->

#### Native addons

- Fixed: loading a `NODE_MODULE` addon in a Worker when the main thread has already loaded it now throws `ERR_DLOPEN_FAILED`, like Node.js.
- Fixed: when a Worker stopped while a native addon's async work was still running, the addon's finalizers ran before that work was done. They now run after it, like in Node.js.

#### Other modules

- Fixed: the `write()` callback on a `node:net` or `node:tls` socket was never called if the connection closed while the write was still pending. It's now called with `ECANCELED`, like in Node.js.

- Fixed: `os.userInfo()` threw `TypeError: Type error` when passed a number or a string. It now behaves like Node.js.
- Fixed: `os.setPriority(priority, undefined)` threw. It now behaves like Node.js.

- Fixed: `socket.connect()` in `node:dgram` failed with `ERR_INVALID_ARG_TYPE` when the port was a numeric string like `"41234"`. Node.js accepts it.
- Fixed: `zlib.crc32(data, undefined)` threw `ERR_INVALID_ARG_TYPE`. It now starts from `0`, like `zlib.crc32(data)`.
- Fixed: `http.maxHeaderSize = "5000000000"` wrapped to `705032704`. The setter now only accepts a safe integer of 1 or more, and a string like `"16384"` throws `ERR_INVALID_ARG_TYPE`.

<!-- https://github.com/oven-sh/bun/commit/ed58ba2126e84127dbb64c29cbddc103a67f77b1 -->

- Fixed: `new SourceMap()` from `node:module` threw `ERR_INVALID_ARG_TYPE` when `mappings` was an empty string, like in the source map of a chunk that only re-exports.
- Fixed: `Profiler.takePreciseCoverage` in `node:inspector` returned function and block ranges that didn't match V8's, so tools like `v8-to-istanbul` marked the wrong lines. A skipped `?? fallback()` was counted as executed.

<!-- https://github.com/oven-sh/bun/commit/6f58f6b89446918c534223c3c32df7feec62abea -->

### Bun APIs

- Fixed: various stability issues in `Bun.serve`, `Bun.spawn`, `FileSink`, `Bun.S3Client`, `Bun.RedisClient`, `Bun.YAML`, `Bun.JSON5`, `Bun.markdown`, `Bun.Cookie`, `Bun.WebView`, `HTMLRewriter`, `bun:ffi`, `Bun.gzipSync()`, `Bun.deflateSync()` and `Bun.unsafe.arrayBufferToString()`.

#### `Bun.serve`

- Fixed: an error thrown by a `Bun.serve` request handler in a Worker is now printed, like on the main thread.

#### `Bun.hash` and `Bun.CryptoHasher`

- Fixed: `Bun.hash()` and its algorithms returned the hash of an empty input when given a `Bun.file()`, an S3 file or a `Bun.build()` output written to `outdir`. They now throw a `TypeError`.
- Fixed: hashing an S3 file with `Bun.CryptoHasher`, `Bun.SHA1.hash()`, `Bun.sha()` or `crypto.hash()` returned the digest of an empty input. It now throws a `TypeError`, like it does for `Bun.file()`.
- Fixed: `Bun.hash()`, `Bun.hash(undefined)` and `Bun.hash(null)` returned the hash of `""`, `"undefined"` and `"null"`. They now throw a `TypeError`, and so do `Bun.hash.crc32()` and the other algorithms.
- Fixed: accessing the static `byteLength` on a subclass of `Bun.MD5`, `Bun.SHA256` or another hasher threw a `TypeError`, and so did `algorithms` on a subclass of `Bun.CryptoHasher`.

#### `Bun.Glob`

- Fixed: `Bun.Glob.match()` treated `**` inside braces like `*`, so `{src/**,lib/**}` didn't match `src/a/b.ts`.
- Fixed: `Bun.Glob.match()` returned `false` for a pattern with more than 10 brace groups, or with braces nested more than 10 deep.

<!-- https://github.com/oven-sh/bun/commit/bfa1209e35663056248406661f1b90851e5c72fa -->

- Fixed: `Bun.Glob`'s `scan()` and `scanSync()` treated a truthy option that isn't `true` as `false`. `{ dot: 1 }` skipped dotfiles and `{ onlyFiles: 1 }` listed directories.

#### Other Bun APIs

- Fixed: `Bun.JSON5.parse()` accepted two invalid inputs. `"1\0 2"` parsed as `1` and `"{a\\u002d: 1}"` as `{ a: 1 }`. Both throw a `SyntaxError`.

<!-- https://github.com/oven-sh/bun/commit/788087c0a4e0c4e57dfe78156ba57a70e329d1b1 -->

- Fixed: `Bun.markdown.ansi()` and `bun file.md` could break a line in the middle of a word made of several parts, like `` `code` `` followed by `.` or `(` followed by a link.

<!-- https://github.com/oven-sh/bun/commit/bfa1209e35663056248406661f1b90851e5c72fa -->

- Fixed: `.ptr` on a `bun:ffi` `cc()` symbol wasn't the address of the C function.

- Fixed: `Bun.YAML.parse()` returned `null` and `Bun.TOML.parse()` returned `{}` when given an S3 file. They now throw a `TypeError`, like they do for `Bun.file()`.

- Fixed: `Bun.gzipSync()` and `Bun.deflateSync()` ignored the `memLevel` and `strategy` options and always compressed with the defaults. A `memLevel` outside 1 to 9 or a `strategy` outside 0 to 4 now throws.

<!-- https://github.com/oven-sh/bun/commit/d0831bc9fe53ceec29281fb9b7dfdebea39bc351 -->

- Fixed: `Bun.CSRF.generate(undefined, options)` threw "Secret is required", so there was no way to pass options and keep the default secret.
- Fixed: `Bun.$.escape()` returned non-string arguments like `null`, `true` or `new String("foo")` unchanged when they didn't need escaping. It now always returns a string.
- Improved: `expiresIn: NaN` in `Bun.CSRF.generate()`, `maxAge: NaN` in `Bun.CSRF.verify()` and a `NaN` port in `Bun.dns.prefetch()` throw a `RangeError` instead of falling back to the default.

<!-- https://github.com/oven-sh/bun/commit/21620e2be3959ed985d971c60565a98a142e4503 -->

- Fixed: `Bun.indexOfLine()` returned `-1` when its first argument wasn't a buffer, such as a string that contains a newline. It now throws a `TypeError`.

<!-- https://github.com/oven-sh/bun/commit/0a2f1c815699d8fd7ab5a7db52493d8e79e34109 -->

- Fixed: `Bun.JSONL.parseChunk()` reported a `read` offset before a value it had already returned when that value was followed by a stray `,`, `]` or `}`. Regression in v1.4.3.
- Fixed: `Bun.JSONL.parse()` and `Bun.JSONL.parseChunk()` returned a value more than once when it was preceded by blank lines and followed by a stray `,`, `]` or `}`. Regression in v1.4.3.

<!-- https://github.com/oven-sh/bun/commit/281698ac9380110414b293017531b39466014ad3 -->

- Fixed: `assetPrefix` on a `Bun.FileSystemRouter` was always `undefined`. It now returns the `assetPrefix` option, or `null` if it isn't set.
- Fixed: `query` and `params` from `Bun.FileSystemRouter`'s `match()` listed numeric keys twice, so `Object.values(router.match("/?0=a").query)` returned `["a", "a"]`.

<!-- https://github.com/oven-sh/bun/commit/3880151f0abacaf7dce9d6d0ab1c7be295d14bb4 -->

- Fixed: `graph.dispose()` on a `Bun.ModuleGraph` leaked the graph's modules when its code still had a `Bun.Terminal` open or a `node:http` server listening. The terminal's file descriptors leaked too.

<!-- https://github.com/oven-sh/bun/commit/392cf216453ac44be360c7fafdffdf509d3c428c -->

### Web APIs

- Fixed: various stability issues in experimental HTTP/2 `fetch()`, `ReadableStream.from()`, `crypto.subtle`, `structuredClone`, `URL`, `URLSearchParams`, `URLPattern`, `Headers` and `FormData`.

#### `fetch`

- Fixed: `fetch()` with `keepalive: false`, or a `keepAlive: false` `Bun.FetchSession`, left its HTTP/1.1 connection open in the pool when the request also had a `Connection: keep-alive` header, which happens when you forward `req.headers`.

<!-- https://github.com/oven-sh/bun/commit/6fbfdcd7efee8cfe8101e5f7afbbcda5d5c0f95e -->

#### Streams and `Blob`

- Fixed: `new Response(blob, { headers }).blob()` set `type` on the original `Blob` when it had none.
- Fixed: `slice()` without a type on a body `Blob` kept the parent's type instead of `""`.

#### Other Web APIs

- Fixed: `Reflect.construct()` of `WebSocket`, `BroadcastChannel` or `Worker` still connected, subscribed or started the thread when the `newTarget`'s `prototype` getter threw.

<!-- https://github.com/oven-sh/bun/commit/39bbb7182cf9eb31a3328195b4198012946f8b9a -->

### TLS

- Fixed: various stability issues in TLS clients and servers.

- Improved: `Bun.serve`, `Bun.listen` and `node:tls` servers now only request a client certificate when `requestCert: true` is set, which matches the docs and Node.js. Setting `ca` alone no longer does. For mTLS, set both.
- Fixed: a `tls.createServer()` with `SNICallback` never negotiated ALPN. `ALPNProtocols` and `ALPNCallback` were ignored, so HTTP/2 clients fell back to HTTP/1.1.
- Fixed: the `ws` package ignored top-level TLS options such as `rejectUnauthorized`, `ca`, `cert` and `key`, so connecting to a self-signed `wss://` server failed with `TLS handshake failed`. `pfx` is also supported.
- Fixed: `node-fetch` ignored the TLS options (`ca`, `cert`, `key`, `rejectUnauthorized`) of an `https.Agent` passed as `agent`.
- Fixed: `fetch()`, `tls.connect()` and other TLS clients failed to connect to servers with an Ed25519 certificate.
- Fixed: a self-signed certificate that isn't marked as a CA, like the ones `dotnet dev-certs` creates, wasn't trusted when passed in `NODE_EXTRA_CA_CERTS` or `ca`.
- Fixed: a TLS server configured with multiple `key` and `cert` pairs only served the last one. With an RSA and an ECDSA pair, each client now gets one it supports.
- Fixed: assigning `tls.DEFAULT_CIPHERS` had no effect on `fetch`, `WebSocket`, `Bun.connect`, `RedisClient`, `Bun.SQL` and `S3Client`.
- Fixed: `Bun.SQL` with `sslmode=prefer` timed out against a Postgres server without TLS. It now falls back to an unencrypted connection, as documented.
- Fixed: `Bun.SQL` ignored `tls.servername`, the `node:tls` spelling of `serverName`.
- Fixed: `tls.connect()` with `autoSelectFamily` added an `'end'` listener for each address it tried, and printed `MaxListenersExceededWarning` after nine.

- Fixed: the `tls` options `key`, `cert`, `ca`, `crl` and `ALPNProtocols` read the entire underlying `ArrayBuffer` when given a view into part of it, like `buffer.subarray(n)`.

### WebSocket

- Fixed: various stability issues in the `WebSocket` client and `ws`.

#### `WebSocket` client

- Fixed: a client `WebSocket`'s `send()`, `ping()` and `pong()` sent the text `[object BuildArtifact]` when given an in-memory `Bun.build()` output. They now send its bytes, like `ServerWebSocket`.

- Fixed: `ws.send()` right before `ws.close()` could lose messages on `ws://` if the server was still sending, while `close` reported a clean 1000.
- Fixed: on `wss://` through a proxy, Close and Pong frames could be sent ahead of queued messages, which were then lost even though the close was reported as clean.
- Fixed: a `wss://` `WebSocket` connected through a proxy closed with 1006 when a frame was sent while a TLS 1.2 server was renegotiating. Direct connections weren't affected.
- Fixed: in rare cases under backpressure, the end of the send queue was never flushed and `bufferedAmount` stayed above 0.
- Fixed: event listeners didn't run in the `AsyncLocalStorage` context of the `new WebSocket()` call.
- Fixed: `new WebSocket(url, { protocols: "chat" })` threw "Value is not a sequence", and `protocol: "chat"` was ignored.
- Fixed: `ws://:token@host` sent no credentials.
- Fixed: one invalid entry in `proxy.headers` silently dropped all of them, including `Proxy-Authorization`. It now throws instead.
- Fixed: a proxy's 407 was reported as "Proxy connection failed", and a CONNECT reply of `HTTP/1.1 200` with no reason phrase, or any 2xx other than 200, was rejected.
- Fixed: `100 Continue` or `103 Early Hints` before the 101 failed the handshake.
- Fixed: `send()`, `ping()` and `pong()` with `Bun.file()` or an S3 file sent an empty frame. They now throw.
- Fixed: non-ASCII header values were sent as UTF-8. They're now sent as Latin-1, like `fetch` and Node.js.
- Fixed: several `WebSocket` TypeScript types. `error` and `onerror` receive an `ErrorEvent`, the `ping` and `pong` events and `binaryType: "blob"` are declared, and `headers` accepts a `Headers` object.

#### `Bun.serve` WebSocket servers

- Fixed: Safari closed the connection with 1002 when a server with `perMessageDeflate: true` sent compressed messages.
- Fixed: `perMessageDeflate.compress` set to `"dedicated"` or `"4KB"` through `"256KB"` was treated as `"3KB"`. Each value now uses its own window size, which uses more memory: `"dedicated"` goes from 149 KB to 282 KB per connection.
- Fixed: `perMessageDeflate: { compress: false, decompress: true }` still compressed outgoing messages, and `decompress: "16KB"` configured the compressor.
- Fixed: `server.stop(true)` closed WebSockets with 1006. They now close with 1001 "Server closed".
- Fixed: after a protocol error, invalid UTF-8 or a message over `maxPayloadLength`, the client saw 1006. It now receives a Close frame with 1002, 1007 or 1009 and a reason.
- Fixed: `idleTimeout: 0` closed idle connections after about 256 seconds instead of never.
- Fixed: `idleTimeout: 65536`, `maxPayloadLength: 2 ** 32 + 16` and `backpressureLimit: 2 ** 32 + 1` wrapped around to 0, 16 and 1.
- Fixed: `maxPayloadLength` also applied to Ping, Pong and Close frames.
- Fixed: `ws.close()` on a socket over `backpressureLimit` dropped messages that `send()` had already accepted, along with the Close frame.
- Fixed: `ws.close()` inside `ws.cork()`, `drain`, or `open` after an async upgrade closed the connection 16 seconds late.
- Fixed: `terminate()`, a throwing `open` handler, `stop(true)` and process exit dropped data that `send()` or `publish()` had accepted in the same tick.
- Fixed: a long `ws.close(code, reason)` reason was truncated in the middle of a character, so the client saw 1007.
- Fixed: `server.upgrade(req, { headers })` sent `Sec-WebSocket-Accept`, `Upgrade` or `Connection` twice when `headers` had them, threw for `headers: {}`, and couldn't decline a subprotocol. An empty `Sec-WebSocket-Protocol` now declines it.
- Fixed: an error thrown after `server.upgrade()` was silently dropped. It's now passed to `error()` or printed to stderr.
- Fixed: `server.reload()` leaked memory for every route of a server with WebSockets, and new `websocket` options didn't apply to sockets that were already open.

#### `ws`

- Fixed: a `ws` server sending a burst of large messages delivered some twice and some out of order.
- Fixed: `ws.once("message")` together with `ws.on("message")` delivered each message twice.
- Fixed: on server sockets, `addEventListener("message")` received a `Buffer` for text, and `onclose` received a number.
- Fixed: the `binary` option of `send()` was ignored for an `ArrayBuffer` or `Blob`, and `{ fin: false }` sent whole messages instead of fragments.
- Fixed: the `send()` callback reported success for a message dropped after `close()`. Callbacks now run once the data is written, or with an error if the socket closes first.
- Fixed: the `send()`, `ping()` and `pong()` callbacks reported success for a frame dropped after the connection ended but before `'close'` fired. They now receive `ERR_STREAM_DESTROYED`, like in Node.js.
- Fixed: messages that arrived before a deferred `handleUpgrade()`, after an `await` or an async `verifyClient`, were lost.
- Fixed: in `'unexpected-response'`, a chunked body arrived undecoded, and a body that came after the response head was lost.
- Fixed: `ws` ignored `proxyRequestOptions.headers` of an `hpagent` proxy agent, so proxies that require `Proxy-Authorization` responded with 407.
- Fixed: headers pushed by a `'headers'` listener were missing from the 101 response (engine.io's cookies), and `handleProtocols: () => false` still selected a protocol (graphql-ws).

#### `node:http` and `undici`

- Fixed: `socket.bytesWritten` was 0 on `'upgrade'` and `'connect'` sockets in `node:http`. engine.io closes such sockets after a second, so a Vite or SvelteKit dev server with socket.io reloaded the page every second.
- Fixed: Slack Bolt in Socket Mode reconnected forever, because `undici` had no `ping()` export and no `undici:websocket:ping` and `undici:websocket:pong` diagnostics channels.

### Runtime

- Fixed: various stability issues in startup on Linux arm64, garbage collection on Linux and uncaught error reporting.

- Fixed: the `^` under a syntax error pointed too far left when double-width characters, like CJK text, came before it on the line.

<!-- https://github.com/oven-sh/bun/commit/bfa1209e35663056248406661f1b90851e5c72fa -->

- Fixed: in JIT-optimized functions, `.sort()` on 16 or fewer integers returned the wrong result with a comparator like `(a, b) => (a + 0.5) - (b + 0.5)`. Regression since v1.4.0.

<!-- https://github.com/oven-sh/bun/commit/d940fbe7b9ca970ab24924387c1a68b41d5c3969 -->

- Fixed: `navigator.hardwareConcurrency`, `os.availableParallelism()` and `process.constrainedMemory()` ignored cgroup v2 limits on Linux hosts that also have a cgroup v1 hierarchy, even just `name=systemd`. Bun's thread counts and GC heap sizing ignored them too.

<!-- https://github.com/oven-sh/bun/commit/b63ebeff30a3533acfa03517a4bc111d48217325 -->

- Fixed: an error thrown right after `let copy = value` at the top of a block could report an earlier statement's line and column, in functions with more than ~110 constants.

<!-- https://github.com/oven-sh/bun/commit/963452075e9d7e2a47a482f297143af5ed4f76c0 -->

- Fixed: `CallSite#getColumnNumber()` counted columns from 0 instead of 1. With `@cspotcode/source-map-support` installed, reading `error.stack` threw "`column` must be greater than or equal to 0" for a frame at column 1.
- Fixed: `error.stack` left out the column of a frame at column 1, printing `file.js:3` instead of `file.js:3:1`.
- Fixed: a stack frame at `const x = require("./x")` pointed at `x` instead of the `require()` call.
- Fixed: an undefined JSX component was reported at its closing tag instead of its opening tag.
- Fixed: the source preview of an uncaught error on the last line of a file with no trailing newline left that line out.

<!-- https://github.com/oven-sh/bun/commit/6f58f6b89446918c534223c3c32df7feec62abea -->

### bun --hot and bun --watch

- Fixed: various stability issues in `bun --hot` and `bun --watch`.

- Fixed: on macOS and Linux, timers under `bun --hot` stopped firing after an uncaught exception, even after you saved a fix. Reloading also resets the exit code to 0.

### bun run

- Fixed: various stability issues in `bun run`, `bunx` and module resolution.

- Fixed: when running as PID 1 in a container, `bun` now exits with 128 + the signal number on Ctrl+C or `docker stop`.

### Transpiler

- Fixed: various stability issues in `Bun.Transpiler` and TypeScript decorators.

- Fixed: `@dec static {}` was accepted. A decorator on a class static block is now a syntax error, like in TypeScript and esbuild.

- Fixed: with standard decorators, when a class decorator returned a new class, the class name inside the class body (decorator arguments, static fields, methods) still referred to the original class.

<!-- https://github.com/oven-sh/bun/commit/5738e23e04cd537ad0076ada37ca530bf8f04a96 -->

- Fixed: hexadecimal, octal and binary number literals above 2^53 could round to the wrong double. For example, `0xb3c8bf81fdf2247` was a different number than in Node.js. `Bun.JSONC.parse()` had the same bug.

<!-- https://github.com/oven-sh/bun/commit/d4be9e3e54e665fd285bd682f5679583f6093bbe -->

- Fixed: `Bun.Transpiler` with `exports.eliminate` left behind imports that only the eliminated exports used. With `trimUnusedImports: false` the imports were kept, and TypeScript output was left with a bare `import "pkg"`.

<!-- https://github.com/oven-sh/bun/commit/3469eb3f863daa34bfcac4fb5ebf15705a1cb404 -->

### bun check

- Fixed: a stability issue in `bun check`.

- Fixed: `bun check` reported false TS2456 and TS2502 circularity errors on Slate's `CustomTypes` module augmentation (`Editor: BaseEditor & ReactEditor`). That turned `Editor` into `any` and hid real errors.
- Fixed: some `bun check` errors depended on the number of threads, like a false TS2321 ("Excessive stack depth comparing types") with `@sinclair/typebox` on two or more threads.
- Improved: `bun check` follows TypeScript 7.1 and no longer reports TS2321. After 100 nested comparisons, it assumes the types are related.

<!-- https://github.com/oven-sh/bun/commit/6bb5dc1956505dd1113455f58d4757c924f3ad54 -->

- Fixed: `bun check` reported TS18046 and TS7006 errors that `tsc` doesn't when narrowing the result of `.then()` on a `Promise<T> | PromiseLike<T>` with `"catch" in result`. This affected tRPC.
- Fixed: `bun check` reported a false TS2589 ("excessively deep") on a call that left out an optional parameter whose type is infinitely deep, like TanStack Router's `useLoaderData()` on recursive JSON data.
- Fixed: `bun check` reported a false TS2589 ("excessively deep") when an object built with a spread, inside a class or generic function, went through a chain of generic calls, like in ts-pattern.
- Fixed: `bun check` reported a false TS2615 ("circularly references itself in mapped type") after a type guard narrowed a class whose method takes immer's `Draft<this>`.
- Fixed: `bun check` reported false TS2344, TS2345, TS2339 and TS7031 errors for some generic types that refer to each other in a cycle and use `infer`.
- Fixed: `bun check` reported a false TS2322 when assigning to a union of intersections where one member holds an unresolved conditional type whose constraint is `never`.
- Fixed: `bun check` reported a false TS2322 when a generic function typed through a generic alias (`Id<string>`) is passed to a generic wrapper and the result is assigned to the same alias.
- Fixed: `bun check` reported a false TS2554 when a class method calls an imported function that has the same name as a type parameter and a method of that class.
- Fixed: `bun check` reported an extra TS2322 on the `return` of a function whose return type annotation references itself, on top of TS2577.
- Fixed: `bun check` reported a TS7023 error that `tsc` doesn't for a generator without a return type that yields a variable in a loop and reassigns it from a call to itself.
- Fixed: `bun check` gave a class declared in a loop a wider type than `tsc` when its `extends` expression reads a variable the loop reassigns, which could produce a TS2322 that `tsc` doesn't report.
- Fixed: `bun check` reported TS7022 where `tsc` reports TS2339 for a destructured parameter with contextual type `unknown` whose default reads another binding, like `take({ first, second = first })`.
- Fixed: `bun check` printed `{0}` in place of the file names in TS18000 when `tsconfig.json` files extend each other circularly.
- Fixed: `bun check` missed errors that `tsc` reports while inferring the type of a variable that's reassigned in a loop, like TS2322 on `last = last.next = item`. This also covers TS2367, TS2774, TS2349 and TS2344.
- Fixed: `bun check` missed TS2367 on a comparison like `y === 5` (with `y: string`) in a value assigned to a loop variable that is read earlier in the loop body.
- Fixed: `bun check` missed TS7022 when a loop reassigns its variable from a value that depends on that variable through a class property, or from a `for...of` variable, like `const { children } = current; for (const child of children) current = child`.
- Fixed: `bun check` missed TS2502 for a setter whose parameter annotation reads the property it declares, like `set degrees(value: (Celsius | Kelvin)["degrees"])`.
- Fixed: `bun check` missed TS2589 when a generic function with a self-referential constraint was passed to a generic call, so a `// @ts-expect-error` above it was flagged as unused.
- Fixed: `bun check` missed TS2540 and TS2704 for a `readonly` property when its type is intersected with a mapped type that removes `readonly`, like `Writable<I> & I`.
- Fixed: `bun check` missed TS2445 and TS2341 for a `protected` or `private` property read through an intersection of its class with a mapped type, like `Record<"balance", unknown> & Account`.
- Fixed: `bun check` took quadratic time in the number of keys of any object in `tsconfig.json`, usually `paths`. 40,000 keys took 10.9 s and now take 0.1 s.
- Fixed: `bun check` took quadratic time when thousands of `paths` or `files` entries in `tsconfig.json` each had an error. 4,000 TS5090 errors took 35.5 s and now take 0.04 s.
- Fixed: `bun check` was slower when the project's directory had tens of thousands of sibling files, like in `$TMPDIR` or a large `~/code`.
- Improved: `bun check -b` parses a shared base `tsconfig.json` once for all projects that extend it. 1,000 projects extending 5,000 `paths` went from 71 s and 3.08 GB to 1.6 s and 0.62 GB.
- Improved: `bun check -b` checks small projects in parallel. It runs up to 16 projects at once instead of 4, so a monorepo of large projects uses more memory.
- Improved: `bun check --no-script` runs the type checker even if `package.json` has a `check` script. It accepts the type checker's flags (`bun check --no-script -b`). `bun fmt` also accepts `--no-script`.

<!-- https://github.com/oven-sh/bun/commit/b92e2f4704824ad8f28509bda363989add0818c7 -->

### bun install

- Fixed: various stability issues in `bun install`, `bun update`, `bun outdated` and `bun run`.

#### Installing

- Fixed: with the hoisted linker, a dependency removed from `package.json` stayed in `node_modules` after `bun install`, along with packages that only it depended on.
- Fixed: with the isolated linker, a dependency stayed linked in `node_modules` and `.bin` after it was manually deleted from `package.json`, or removed with `bun remove` inside a workspace or while another package still depended on it.
- Fixed: switching between the hoisted and isolated linkers left parts of both layouts in `node_modules`, plus a `.old_modules-*` copy of the old tree.
- Fixed: an install that was killed partway could leave a half-written package in `node_modules` that later installs treated as installed.
- Fixed: with the hoisted linker, a `file:` folder outside the project (`file:../plugin`) couldn't load its own dependencies, and on Windows the install failed with `EPERM`.
- Fixed: `file:` folders and local tarballs were reinstalled on every `bun install` with the hoisted linker.
- Fixed: without a lockfile, `bun install --production` failed when a `workspace:`, `link:` or `file:` devDependency pointed to a missing folder, like when a Docker image only contains one package from a monorepo.
- Fixed: `bun install` failed with `Failed to link <pkg>: EEXIST` on an up-to-date `node_modules` that's mounted read-only.
- Fixed: `bun install --dry-run` ran the project's lifecycle scripts, and `dryRun = true` in `bunfig.toml` was ignored.
- Fixed: `bun install --dry-run` printed one line per dependency edge, skipped peers and ignored the platform. It now lists each package the install would add to `node_modules` once.
- Fixed: `bun install --analyze` and `bun add --analyze` skipped packages that are only imported by subpath, like `lodash/debounce`.
- Fixed: with the isolated linker, an optional dependency whose tarball couldn't be downloaded failed the install. It's now skipped.
- Fixed: `bun install <pkg>`, `bun link` and `bun update -i` exited with code 0 after printing `Failed to install N packages`.
- Fixed: a relative `--cwd` was applied twice by `bun update`, `bun link`, `bun unlink` and `bun why`, so they failed with `ENOENT`.
- Fixed: `bun install` left extraction directories in `$TMPDIR` when two installs shared a cache or a tarball failed to extract, which filled the disk on long-lived CI machines.
- Fixed: two installs sharing a cache on NFS could delete each other's entries and fail with `ENOENT`.
- Fixed: with `NODE_ENV=development` or `test`, `bun install` read `.env.production` instead of the `.env` files that `bun run` reads.
- Fixed: `bun --bun run` only worked for the first user on a shared machine.
- Fixed: on Alpine Linux (musl), `Bun.which()` returned a file that isn't executable.

#### Resolving versions

- Fixed: `1.0.0 || ^2.0.0` resolved to `1.0.0`. A range that starts with an exact version was parsed as only that version.
- Fixed: `bun add`, `bun install` and `bun update --latest` failed for a package whose `latest` dist-tag is missing or points to an unpublished version. They now fall back to the highest release.
- Fixed: dist-tags that start with a digit, such as `pkg@1beta`, were parsed as version ranges.
- Fixed: an auto-installed peer dependency got the newest version that one dependent allows. It now gets the newest version that all of them accept.
- Fixed: a peer dependency on `bun` downloaded the `bun` npm package, which is 80 to 170 MB.
- Fixed: a version published after Bun cached the package's metadata couldn't be installed for up to 5 minutes.
- Fixed: in a monorepo with a lockfile, adding a dependency whose name another workspace uses for an `npm:` alias requested a garbled name, such as `GET /1.0.0no-deps - 404`.
- Fixed: editing a workspace's `package.json` re-downloaded its GitHub and tarball dependencies and re-fetched its git dependencies, even though `bun.lock` had already resolved them.
- Fixed: `file:`, tarball and git packages with no `name` in their `package.json` got broken `bun.lock` entries. They're now named after their source.
- Fixed: an `overrides` key with a prerelease range, such as `pkg@>=5.0.0-alpha.0 <8`, also replaced dependencies on `^4`.
- Fixed: `bun add foo` and `bun update --latest` installed a version that a version-scoped override like `"foo@>=2.0.0": "1.2.0"` forbids.
- Fixed: `Bun.semver.satisfies()` returned `true` for versions that aren't valid semver, such as `""`, `"1.2.3.4"` and `"1.2.3 junk"`, and `false` for `1.0.0+build-1` against `^1.0.0`.
- Fixed: `Bun.semver.satisfies()` and `bun install` parsed build metadata after a bare major (`^2+x`) as a second range, which allowed a different major version.
- Fixed: `Bun.semver.order("2+1", "2.0.0")` threw.
- Fixed: `>=0.5 0` was parsed as two alternatives instead of an intersection, and `<x` and `>x` matched every version.

#### Hoisted and isolated linkers

- Fixed: the hoisted linker could place a package next to a version of its peer dependency that's outside the peer's range.
- Fixed: with the hoisted linker, `--filter`, `--production`, `--omit`, `--os` and `--cpu` changed where packages were placed. Switching flags could leave a workspace loading the wrong version. Placement now only depends on `bun.lock`.
- Fixed: with the hoisted linker, a dependency nested under a package with `bundledDependencies` could load the bundled copy instead of the version it asked for.
- Fixed: with the hoisted linker, `bun prune` deleted a package's bundled dependencies when `bun.lock` didn't mark them as bundled, which affected `aws-cdk-lib`.
- Fixed: `bun add -g` didn't link the package's binaries into the global bin folder when `linker = "isolated"` was set in `~/.bunfig.toml`.
- Fixed: with the isolated linker, packages that are peer dependencies of each other were installed twice when different workspaces or dependencies pulled them in.
- Fixed: with the isolated linker and an `npm:` alias that installs a second copy of a package, `node_modules/.bun/node_modules/<name>` could point at a different version after each install.
- Fixed: the isolated linker replaced a symlinked `node_modules` with a real folder. Both linkers now install through the symlink, and cleanup doesn't delete anything behind it.
- Fixed: with the isolated linker on macOS and Linux, a workspace whose folder or parent folder is a symlink ended up with dangling dependency symlinks.
- Fixed: the isolated linker silently dropped symlinks inside a `file:` folder dependency. Both linkers now keep symlinks that point inside the folder, copy symlinks to other project files as regular files, and skip the rest with a warning.
- Fixed: with `globalStore = true`, TypeScript couldn't find the project's `@types/*` from inside a shared package.
- Fixed: `BUN_INSTALL_GLOBAL_STORE=false` (also `no`, `off` and an empty value) enabled the global store.
- Fixed: symlinks to a renamed or removed workspace were left in `node_modules`.
- Fixed: with the hoisted linker, `bun install` didn't link a newly added workspace when the registry package of the same name and version was already in `node_modules`.
- Fixed: on Alpine Linux, a package that ships native binaries for both libcs, such as `@anthropic-ai/claude-code`, could have its command linked to the glibc binary.
- Fixed: none of a package's binaries were linked if one value in its `bin` object wasn't a string.
- Fixed: when several packages declare the same binary, which one ended up in `.bin` was arbitrary. Direct dependencies now take priority.
- Fixed: a binary that's generated by the package's own install script, like a workspace CLI built in `prepare`, wasn't linked into `.bin` until the next `bun install`.
- Fixed: `bun install` failed on file systems that don't support symlinks. It now warns once and skips the `.bin` links.

#### Lockfile and `--frozen-lockfile`

- Fixed: a fresh `bun install` could write a different `bun.lock` depending on which registry response arrived first. Dependencies are now resolved in a deterministic order.
- Fixed: `bun ci` failed in an unchanged monorepo when a workspace had a lifecycle script and a dependency on `latest`, as soon as a newer version of that dependency was published.
- Fixed: `bun ci` applied `minimumReleaseAge` to a version already pinned in `bun.lock` and failed when `package.json` expressed the same version differently, such as with `catalog:`.
- Fixed: a direct dependency locked at a version outside its `package.json` range, which can happen after a bad merge, was installed anyway and passed `--frozen-lockfile`.
- Fixed: `bun install` kept a locked version that an `overrides` rule forbids, which can also happen after a bad merge. It now re-resolves that entry, and `--frozen-lockfile` fails with an explanation.
- Fixed: `bun install` could write a `bun.lock` it couldn't read back (`Failed to resolve prod dependency`) when a package bundles a package that another package lists as an optional peer.
- Fixed: `bun <file>.lockb` printed the lockfile from the file's directory instead of the file itself. That broke using it as a git `textconv`, where git passes a temporary copy.
- Fixed: `bun pm migrate` left `trustedDependencies` out of the `bun.lock` it wrote, so `bun pm untrusted` listed trusted packages as blocked.
- Fixed: a `yarn.lock` or `pnpm-lock.yaml` that pins a GitHub dependency failed on the first `bun install` and lost the pin on the second. The pinned commit is now installed, including for pnpm's `type: git` resolutions.
- Fixed: migrating a Yarn v1 lockfile failed with "failed to resolve" on `link:` and bare path (`./dir`) entries, and in a monorepo where a registry package depends on a workspace.
- Fixed: migrating a Yarn v1 lockfile merged a tarball or hosted-git entry with the registry package of the same name and version, so a fork was replaced by the published package.
- Fixed: a lockfile that Bun can't migrate, like npm 6's `package-lock.json`, prevented Bun from migrating a `yarn.lock` or `pnpm-lock.yaml` next to it. Bun now warns and tries the next one.
- Fixed: a single `package-lock.json` entry from another host without an `integrity` made Bun ignore the whole lockfile. The entry now keeps its version and gets its integrity from the configured registry, with a warning.

#### Workspaces and catalogs

- Fixed: a workspace whose `package.json` has no `name` failed the whole install. It's now skipped.
- Fixed: depending on `latest` of a sibling workspace that isn't published failed with a 404. It now links the workspace.
- Fixed: a workspace was left out of the install when the root `package.json` depends on a registry package with the same name. It now keeps its `bun.lock` entry and its dependencies are installed.
- Fixed: `bun add <name>` inside a workspace installed the package from the registry when `<name>` is also a sibling workspace. It now links the workspace and writes `workspace:*`.
- Fixed: `bun add alias@npm:<workspace>` wrote `"alias": "workspace:*"`, which every later install rejected.
- Fixed: `bun link <package>` inside a workspace that has `workspace:` or `catalog:` dependencies failed with `Workspace dependency "bar" not found` or `is not in the catalog`.
- Fixed: negated `workspaces` entries written as `!./pkg` or `!pkg/` were ignored, and `bun run --filter` and `--workspaces` didn't apply negated entries.
- Fixed: workspace globs skipped symlinked directories.
- Fixed: `bun update <name>` rewrote non-registry entries in `package.json`, for example `workspace:^` to `workspace:*`.
- Fixed: `bun run --filter <pkg> deploy` still ran `deploy` and `postdeploy` after `predeploy` failed. It now skips them, like `bun run` without `--filter`.

#### Registries, `.npmrc` and `bunfig.toml`

- Fixed: a registry URL with a path and no trailing slash, such as `https://host/npm/repo`, lost its last path segment, so every request got a 404.
- Fixed: `.npmrc` credentials are now matched the same way npm matches them. A `//host/` entry applies to every path under it, including a registry set by `--registry` or an environment variable.
- Fixed: `~` in `cache.dir`, `globalDir`, `globalBinDir` and `cafile` created a directory literally named `~`.
- Fixed: a relative `cafile`, `cache`, `globalDir` or `globalBinDir` in `bunfig.toml` or `.npmrc` is now resolved relative to that file. A relative `cafile` used to only work from the project root.
- Fixed: a relative `--config`, `--cache-dir` or `--cafile` is now resolved relative to the current working directory instead of the project root.
- Fixed: `-g` commands ignored `globalDir` from `--config` and `~/.bunfig.toml`.
- Fixed: `bun publish` and `bun pm whoami` ignored `http_proxy`. Those two and `bun audit` also ignored `NODE_TLS_REJECT_UNAUTHORIZED=0`.
- Fixed: a redirected registry request ignored `NO_PROXY` for the new URL. Each redirect now picks its proxy based on its own URL, like `fetch()`.
- Fixed: credentials embedded in a registry URL were sent percent-encoded, so `p%40ss` wasn't sent as `p@ss`.
- Fixed: `BUN_CONFIG_MAX_HTTP_REQUESTS` was ignored.

#### Tarballs and the cache

- Fixed: `--force` and re-adding the same path or URL kept the old contents of a tarball dependency, and `bun update` left its old hash in `bun.lock`.
- Fixed: `file:` and URL tarballs were cached by path or URL, so two projects or branches with different files at the same path got each other's package.
- Fixed: two registries on the same host that differ only in port, path or protocol shared cache entries.
- Fixed: uncompressed `.tar` packages of 2 MB or more, and `.tgz` files of that size served with `Content-Encoding: gzip`, failed to install.
- Fixed: `Bun.Archive`'s `extract(dir, { glob })` and `files()` resolved with the entries read so far on a truncated or corrupted archive. They now reject, like `extract(dir)`.

#### Git dependencies

- Improved: git dependencies now require Git 2.24 or newer.
- Fixed: a cached git dependency never picked up tags and branches pushed after it was cached. Bumping to a new tag failed with `no commit matching`, and `bun update` stayed on the old commit.
- Fixed: `git+ssh://git@host:2222/...` lost its port when Bun fell back to ssh.
- Fixed: a git dependency with an ssh URL never fell back to ssh when the https attempt returned "not found".
- Fixed: `bun add name@git+ssh://host/repo.git` failed with `unrecognised dependency format` for hosts other than GitHub.
- Fixed: `deploy@host:org/repo.git` was cloned as `git@deploy@host`.
- Fixed: `gitlab:`, `bitbucket:`, `gist:` and `sourcehut:` dependencies failed to install from `bun.lock` on a clean machine. They're now written as `git+ssh://git@gitlab.com/user/repo#<sha>`, and existing entries still install.
- Fixed: an optional git dependency that can't be cloned failed the install. It's now skipped with a warning.
- Fixed: `bun install` and `bun create` run from a git hook operated on the hook's repository, because `GIT_DIR` and similar variables were passed on to git.
- Fixed: `bun install` overrode your `core.sshCommand` or `core.askPass` for git dependencies when it's set outside `~/.gitconfig`, like in `~/.config/git/config` or an included file.
- Fixed: `submodule.recurse = true` in your git config broke a git dependency that has a submodule.
- Fixed: a cached clone that git can't use, because of a stale lock file or a damaged ref store, failed every install. It's now re-cloned.

#### Lifecycle scripts and trust

- Fixed: with the isolated linker, a package added to `trustedDependencies` after it was installed never ran its scripts.
- Fixed: a package installed through an `npm:` alias couldn't be trusted consistently. It's now trusted by its real name in `package.json`, `--trust`, `bun pm trust` and `bun.lock`.
- Fixed: `--concurrent-scripts` and `concurrentScripts` had no effect with the isolated linker, and the `bunfig.toml` value took precedence over the flag.
- Fixed: `bun pm untrusted`, `bun pm trust` and `bun pm ls --trusted` used a stale lockfile instead of `package.json`.
- Fixed: `bun pm pack`, `bun publish` and `bun pm version` ran lifecycle scripts without `node_modules/.bin` in `PATH`, so `"prepack": "tsc"` failed with `command not found`.
- Fixed: a regression in Bun v1.4.0 where a failed lifecycle script's non-UTF-8 output, such as Latin-1 or Shift-JIS compiler errors, was printed with every invalid byte replaced by U+FFFD.

#### `bun add`, `bun remove`, `bun update` and `bun outdated`

- Fixed: removing the last package in a dependency group with `bun remove` reordered the other keys in `package.json`.
- Fixed: `bun add ../path` run from a subdirectory of the project, and `bun install -g .`, resolved the path from the wrong directory.
- Fixed: `bun add` with a folder, tarball or git URL wrote the package into `bun.lock` twice when `package.json` already listed it from another source.
- Fixed: the first `bun add -g` on a machine edited a `package.json` above the global directory, such as `~/package.json`.
- Fixed: `bun add --dry-run` in an empty directory created a `package.json`.
- Fixed: `bun add -E react@18` wrote `18`. `--exact` now pins the range you typed to the installed version, like npm.
- Fixed: `bun add`, `bun remove`, `bun update` and `bun patch` rewrote `package.json` even when nothing changed.
- Fixed: commands that rewrite `package.json` turned control characters in strings into `\x1B` or `\v`, which isn't valid JSON.
- Fixed: `bun update` printed `(no changes)` when it had rewritten `package.json`.
- Fixed: `bun update --latest` downgraded a dependency written as a dist-tag like `"canary"` when that tag is ahead of `latest`. `bun update -i` no longer offers an older `latest` either.
- Fixed: `bun update --latest` downgraded a `catalog` entry that was ahead of the `latest` tag. This affected pinned versions, ranges and dist-tags like `canary`.
- Fixed: `bun update --interactive` rewrote ranges like `>=1.0.0 <2`, `1.x` and `^1 || ^2` as their first character plus the chosen version. They're now left unchanged if they already allow the chosen version.
- Fixed: `bun update --exact` pinned `peerDependencies` ranges, and `bun update --latest` narrowed a peer range that already allows the latest version.
- Fixed: `bun outdated a b` only listed packages matching both patterns. It now lists packages matching either one, and `!pattern` excludes.
- Fixed: `bun outdated` hid dependencies that are ahead of the `latest` tag. It now lists them if `bun update` would change them.

#### `bun pm` and `bun why`

- Fixed: `bun pm bin -g` and `bun pm ls -g` failed with `No package.json was found` before the first global install.
- Fixed: `bun pm ls` listed the lockfile's packages under a `node_modules` label. When `node_modules` exists, it now lists what's actually installed.
- Fixed: `bun why name@version` found nothing for exact versions, scoped names and workspaces.
- Fixed: `bun why` printed a package's dependents again at every occurrence, which grew exponentially in large trees. Repeats are now marked `*deduped`.
- Fixed: `bun pm version` didn't create a commit or tag in a git worktree or submodule, where `.git` is a file.
- Fixed: `bun pm version <increment>` now computes the same version as `npm version`. `1.0.0-rc.1` plus `major` is `1.0.0`, not `2.0.0`, and `v2.0.0` is written as `2.0.0`.
- Fixed: `bun pm view pkg@tag` silently used `latest` for a dist-tag that doesn't exist. It's now an error that lists the available tags.
- Fixed: `bun pm cache rm` deleted the cache directory itself, which left a dangling link when it's a symlink and failed with `EACCES` in a read-only parent.
- Fixed: `bun pm cache rm` printed `Cleared 'bun install' cache` without deleting anything when `~/.bunfig.toml` set the cache directory.

#### `bun pm pack` and `bun publish`

- Fixed: `workspace:` and `catalog:` versions came from `bun.lock`, so they could be stale, and packing failed without a lockfile. They now come from the `package.json` files.
- Fixed: a `catalog:` dependency whose catalog entry is `workspace:^` or another `workspace:` form was published as-is. It's now replaced with the workspace's version.
- Fixed: `bun pm pack` and `bun publish` omitted `bundledDependencies` with the isolated linker and in workspaces.
- Fixed: `!pattern` in `.npmignore` couldn't re-include files below an excluded directory.
- Fixed: relative `--destination` and `--filename` paths, and `bun publish ./x.tgz`, were resolved from the package root instead of the current directory.
- Fixed: `bun publish --provenance`, `publishConfig.provenance` and `NPM_CONFIG_PROVENANCE` were ignored, so the package was published without provenance. They're now errors until Bun supports generating it.
- Fixed: a file listed under several `bin` names was packed several times.

#### `bun patch`

- Fixed: a patch that adds a file in a new directory failed with `EACCES` for non-root users.
- Fixed: hunks were applied by line number alone, so a patch could land in the wrong place after the file changed. They're now matched by context, like `git apply`.
- Fixed: a patch that appends lines to a file lost the last line when the file has no trailing newline before or after the change.
- Fixed: `bun patch --commit` on an already patched package wrote `.bun-tag` files and cache paths into the patch.
- Fixed: `bun patch` dropped the package's bundled and nested dependencies, and `--commit` recorded them as deleted.
- Fixed: a `bun patch` that failed partway left a partial package, or none, in `node_modules`.
- Fixed: a `bun patch --commit` whose patch doesn't apply, or whose install is refused, could leave the package folder, `package.json` or `bun.lock` changed. Now nothing is modified until every check has passed.
- Fixed: with the isolated linker, `bun patch` replaced `node_modules/<pkg>` with a directory that couldn't load its dependencies.
- Fixed: `bun patch` and `bun patch --commit` failed with `ENOENT` when the package is installed but no longer in the cache, for example after `bun pm cache rm`.
- Fixed: `bun patch --commit` failed when a patched file or the cache folder has a non-ASCII name.
- Fixed: `bun install` silently skipped the parts of a patch it can't apply, like a `Binary files ... differ` section, and a `patchedDependencies` key without a version. Each one now prints a warning.

#### `bunx`

- Fixed: `bunx pkg@latest` relinked everything on every run, and a copy more than a day old was refreshed on every run.
- Fixed: several `bunx <pkg>` processes started at the same time corrupted their shared install.
- Fixed: `bunx <tool>` with a cached copy older than a day failed when the registry returned 5xx, 429 or 408, or when a newer version's tarball couldn't be fetched.
- Fixed: `bunx pkg@3` failed with `could not determine executable to run` in a project where another version of `pkg` is installed and has no such command.
- Fixed: `bunx @scope/pkg` could run another package's `.bin` entry of the same name instead of that package's own command.

#### `bun create`

- Fixed: `bun create <local template> <dir>` deleted an existing directory. It now merges, lists conflicts and only overwrites with `--force`.
- Fixed: `git init` and the first commit raced the template's tasks and `bun install`. The first commit now contains exactly the template.
- Fixed: `bun-create` tasks ran without a shell, didn't run at all in a template without dependencies, and their failures were ignored.
- Fixed: the indentation of the template's `package.json` was lost.

#### Auto-install

- Fixed: timers and I/O callbacks could run inside `require()`, `import()` and `Bun.resolveSync()` while auto-install fetched a package.
- Fixed: importing a second version (`pkg@2` after `pkg@1`) failed with `Unexpected` when the package's metadata was in the disk cache.
- Fixed: one failed auto-install download limited `fetch()` to 4 concurrent requests for the rest of the process.
- Fixed: peer dependencies of auto-installed packages failed with "Cannot find module".
- Fixed: with auto-install, an import throws "Cannot find package" when the cache or temp directory can't be written, like in a read-only container.

#### Windows

- Fixed: commands whose shebang uses `env -S` didn't run.
- Fixed: `.bunx` shims broke when the package is on another drive.
- Fixed: `bun remove` left `.bunx` and `.exe` shims behind.
- Fixed: `bun install` failed in the root of a drive.
- Fixed: `EINVAL: Failed to replace old lockfile` on exFAT and FAT32 drives.
- Fixed: renames failed while a virus scanner held a new file open. They're now retried for up to 5 seconds.
- Fixed: `bun create <local template>` failed with "ENOENT: failed to copy file" when the working directory, the template or the destination has a non-ASCII character.
- Fixed: the `preversion`, `version` and `postversion` scripts of `bun pm version` ran with an empty environment in a git repository.

### JavaScript bundler

- Fixed: various stability issues in `bun build`, `Bun.build` plugins and macros.

- Fixed: `bun build --minify` gave two default exports the same name when each was a parenthesized named function or class expression, like `export default (function inc() {})`.
- Fixed: `bun build` left an HTML `<img src>` pointing to the source path without writing the file when a stylesheet inlined the same image through `url()`.

- Fixed: in source maps from `bun build --react-compiler`, `catch` and the `{` after it mapped to the position of `try`.

<!-- https://github.com/oven-sh/bun/commit/491aaad15f493db96ad83da99592e45815bf75be -->

- Fixed: `bun build` source maps mapped `export default function`, `export default class`, `export async function`, `abstract class`, `import()`, `require()` and `require.resolve()` to the wrong column.

<!-- https://github.com/oven-sh/bun/commit/6f58f6b89446918c534223c3c32df7feec62abea -->

### bun build --compile

- Fixed: various stability issues in `bun build --compile` and compiled executables.

### CSS Parser

- Fixed: a stability issue in the CSS parser.

- Fixed: `bun build` dropped `translate`, `rotate` or `scale` declared before `transform` in the same CSS rule, and merged the ones declared after it into `transform`.

<!-- https://github.com/oven-sh/bun/commit/3da4e2541270d5a859fc1630ec58042f369f525e -->

### Dev server

- Fixed: a stability issue in the dev server.

- Fixed: `bun ./index.html` ignored `--port 3000` and `--host 0.0.0.0` when written with a space instead of `=`.
- Fixed: `bun ./index.html --host=::1` threw a `RangeError`.
- Fixed: `bun ./index.html` printed a startup error twice, such as the one for an invalid `--port`.

<!-- https://github.com/oven-sh/bun/commit/ed680823cb629699012188c9eb38907df7876e52 -->

- Fixed: the underline under a CSS resolve error for `url('/fonts/a.woff2')` ended 5 characters early. It now covers the whole `url(...)`, and only the path in `composes: x from "./a.css"`.

<!-- https://github.com/oven-sh/bun/commit/91e7e3b140eaef348b7bc33d564dcb67b7de4ddd -->

### bun test

- Fixed: various stability issues in `bun test` and `bun test --changed`.

- Fixed: `bun test --update-snapshots` emptied a snapshot file when the run ended early, like with `--bail`. The old contents are now kept until the new ones are written.

- Fixed: `bun test --coverage` counted skipped code as covered: the right side of `||`, `&&`, `??` or `?:` on its own line, code skipped by `?.`, `||=`, `&&=`, `??=` or an unused default value, and an untaken `break`.
- Fixed: `bun test --coverage` counted the lines after an `await` or `yield` as covered when the function never resumed past it. `% Lines` can drop after upgrading, so check your `coverageThreshold`.
- Fixed: `bun test --coverage` listed the wrong `Uncovered Line #s` for a function that never ran. The range could start two lines early and miss the last line.
- Fixed: `bun test --coverage` reported lines that ran as uncovered, such as a `break` in a block inside `while (true)`, or the `import` above `class Derived extends Base() {}`.
- Fixed: `bun test --coverage` ignored a line with only `default:`, or `case` with its value on the next line, in functions that ran.
- Fixed: `bun test --coverage` listed the `{` line instead of the `else` line when `else` and `{` were on separate lines. Source maps gave `else` the position of the statement after it.
- Fixed: `bun test --coverage` listed lines at the top of a file as code, comments included, when the file used a `--define` name whose value is a non-empty object or array.
- Fixed: `% Funcs` in `bun test --coverage` counted implicit constructors, class field initializers, `static` blocks, and generator and async function bodies as functions. A fully tested class could show 50%.
- Fixed: the `DA:` hit counts in `lcov.info` weren't the number of times a line ran. A line that ran once could show 14.

<!-- https://github.com/oven-sh/bun/commit/6f58f6b89446918c534223c3c32df7feec62abea -->

### Bun Shell

- Fixed: a stability issue in Bun Shell.

- Fixed: `` $`cmd` `` truncated an environment variable's value at a NUL byte. `.env()` and `.cwd()` now throw `ERR_INVALID_ARG_VALUE` for strings containing a NUL byte.
- Fixed: in Bun Shell, `mv -n` overwrote an existing destination.
- Fixed: in Bun Shell, `cp -n` exited with code 1 when the destination existed.

### SQL

- Fixed: various stability issues in `Bun.SQL`, `bun:sqlite` and `node:sqlite`.

#### `Bun.SQL` connection pool

- Fixed: `idleTimeout` and `maxLifetime` closed a connection that was running a query, was reserved, or had an open `sql.begin()` transaction. Now the connection finishes its work first.
- Fixed: a pool that lost some of its connections didn't open new ones as long as one was still usable, so it never grew back to `max`.
- Fixed: a connection released to the pool inside an open transaction, such as a reservation after a manual `BEGIN`, was handed to the next caller. The pool now closes it.
- Fixed: `sql.close({ timeout: 0 })` waited for running queries instead of closing immediately.
- Fixed: `sql.close({ timeout: 2 ** 31 })` overflowed the timer and killed the running queries after 1 ms.
- Fixed: `sql.begin()` and `sql.reserve()` jumped ahead of older queries that were waiting for a connection. Callers are now served in FIFO order.
- Fixed: `onconnect` and `onclose` never fired for the `sql.listen()` connection.
- Improved: PostgreSQL and MySQL sockets now use TCP keepalive (60 seconds), so a query fails instead of waiting forever when the server disappears.

#### `Bun.SQL` transactions

- Fixed: in `sql.begin(tx => Promise.all(tasks))`, after one task failed, statements that the other tasks sent after the `ROLLBACK` ran in autocommit mode, so their rows were kept. They now reject.
- Fixed: a query created on a `reserved` or `tx` handle but first awaited after `release()`, `COMMIT` or `ROLLBACK` ran on a connection that could belong to another caller. It now rejects.
- Fixed: `reserved.release()` during a running `reserved.begin()` returned the connection early, so that transaction's `COMMIT` could commit the next caller's work.
- Fixed: `tx.close({ timeout })` committed the transaction when its queries finished in time. It now rolls back.
- Fixed: the `sql.begin()` callback saw no `AsyncLocalStorage` store on a cold pool, and another request's store on a busy one. It now sees its caller's.
- Fixed: a result returned from a `sql.begin()` or `tx.savepoint()` callback was returned as a plain array without `count` and `command`.
- Fixed: `sql.begin()` and `sql.beginDistributed()` (PostgreSQL, MySQL) rejected a transaction the server had committed or prepared when the server's response and its shutdown error arrived in the same read.

#### `Bun.SQL` queries and results

- Improved: a `` sql`...` `` call compiled by Babel's or swc's loose mode for template literals now throws a `TypeError`. Disable loose mode for template literals.
- Fixed: `affectedRows` was always `null` on PostgreSQL and SQLite. It's now the number of rows written, and `0` for a `SELECT`. On MySQL, `count` was `0` after a write.
- Fixed: `WHERE id IN ${sql([])}` was a syntax error on PostgreSQL and MySQL. It now matches no rows.
- Fixed: on MySQL and MariaDB, `sql("db.table")` quoted the whole string as a single identifier. It's now `` `db`.`table` ``, as documented.
- Fixed: on MySQL, `ON DUPLICATE KEY UPDATE ${sql({ age: 31 })}` was a syntax error when the clause was lowercase or split across lines.
- Fixed: `sql instanceof Bun.SQL` threw a `TypeError`.
- Improved: in TypeScript, the awaited result of a query has `count`, `command`, `affectedRows`, `columns` and the other result properties.

#### `Bun.SQL` connection options

- Improved: `Bun.SQL` no longer reads the `PASSWORD` environment variable. Use `PGPASSWORD`, `MYSQL_PASSWORD`, the URL or the `password` option.
- Fixed: `new SQL({ filename: "./dev.db" })` without `adapter` tried to connect to PostgreSQL, and `SQLITE_URL=x.db` tried to open a file named `postgres://x.db`.
- Fixed: the documented `socket` option and `?socket=` had no effect, and `mysql://user:pass@/db?socket=/path` threw `Invalid URL`.
- Fixed: a `path` that doesn't exist was ignored, and Bun connected over TCP. The connection now fails with `ERR_POSTGRES_CONNECTION_REFUSED` or `ERR_MYSQL_CONNECTION_REFUSED`.
- Fixed: `PGUSERNAME` and `USERNAME` were documented as the PostgreSQL user but ignored.
- Fixed: a `mariadb://` URL without a database tried to select a database named `mariadb` and failed with errno 1049. It no longer selects one.
- Fixed: with the SQLite adapter, `?mode=rw` created the file, and `?mode=RO` or `{ readwrite: false }` opened it read-write.

#### `Bun.SQL` with PostgreSQL

- Fixed: each distinct combination of `NULL` parameters prepared another server-side statement, and none of them were ever closed, so backends grew to several GB. Each connection now keeps its 1,024 most recent statements.
- Fixed: after `ALTER TABLE`, `DEALLOCATE ALL` or `DISCARD ALL`, a cached statement kept failing, for example with `cached plan must not change result type`. Bun now re-prepares it.
- Fixed: `sql.array(["a", "b"])` without a type was cast to `JSON[]`, so a `text[]` column stored JSON-quoted elements. Bun no longer adds a cast.
- Fixed: `null` in `sql.array(values, "TEXT")` or `"INT"` became the string `"null"` or an error. It's now SQL `NULL`.
- Fixed: a prepared query that returned an `int4[]` or `float4[]` with a `NULL` or more than one dimension rejected with `ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET` or `ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET`.
- Fixed: a `Date` bound to `text` was stored as its `toString()`, and a plain object as `[object Object]`. They're now stored as ISO 8601 and JSON.
- Fixed: a `Date` bound to `date` or `time` failed.
- Fixed: with `prepare: false`, a `Date` was sent as its `toString()`, and an object bound to `jsonb` as `[object Object]`.
- Fixed: an array or object bound to a `bool`, `int4`, `float8` or `timestamp` parameter was coerced on the client, so `[]` became `true`. It's now sent as text and validated by the server.
- Fixed: an Invalid Date was stored as 1970-01-01 on PostgreSQL and MySQL. The query now rejects.
- Fixed: whole numbers from 2^51 to 2^53 - 1 were sent as `float8`, so `numeric` stored 2^52 + 1 as `4503599627370500`. They're now sent as `int8`, which Bun returns as a string.
- Fixed: `COPY ... FROM STDIN` and `COPY ... TO STDOUT` as plain queries, without a stream, now reject with `ERR_POSTGRES_COPY_NOT_SUPPORTED`.
- Fixed: `PostgresError.column` and `.line` pointed to a position in Bun's own source code when the server didn't send one.

#### `Bun.SQL` with MySQL and MariaDB

- Fixed: queries on the same connection were sent one at a time instead of pipelined, a regression since v1.2.22. With 30 ms of latency, four prepared queries now take 31 ms, down from 124 ms.
- Fixed: without TLS, `caching_sha2_password` logins failed with errno 1045 for passwords of 20 bytes or more. Fast authentication also never succeeded, so plain TCP needed `allowPublicKeyRetrieval: true` on every login.
- Fixed: `sha256_password` was documented, but every login failed with errno 1045.
- Fixed: a single `INSERT` with K nullable columns could prepare up to 2^K statements on the server, which were never closed. Each connection now keeps its 256 most recent statements.
- Fixed: a failed prepare was cached, so the query kept failing after a `CREATE TABLE` had made it valid.
- Fixed: a query on a connection that had been idle didn't keep the process alive, so the process could exit with code 0 while the query was in flight.
- Fixed: two queries on one connection could resolve with each other's rows when a parameter's `toJSON()` started a query.
- Fixed: MariaDB rejected every object or array parameter with errno 1210.
- Fixed: a `Date` before 1970 was rejected, and a prepared query lost the fractional seconds of `TIME(n)` columns.
- Fixed: `lastInsertRowid` above 2^53 - 1 was rounded. It's now returned as a string, or a `BigInt` with `bigint: true`.
- Fixed: `Bun.SQL` threw `ERR_MYSQL_UNSUPPORTED_PROTOCOL_VERSION` when a MySQL or MariaDB server rejected a new connection, such as when it hit `max_connections`. It now throws the server's error (`errno` 1040, "Too many connections").

#### `Bun.SQL` with SQLite

- Fixed: a valid query resolved to `[]` when its text was written like `select*from t` or `/*c*/select`, or was `values (...)`.
- Fixed: `INSERT ... SELECT` and `WITH ... INSERT` reported a `count` of 0.
- Fixed: a string of several statements whose first statement returns rows, such as a migration file that starts with a `PRAGMA`, only ran that statement.
- Fixed: a second `sql.begin()` while another one was running failed with `cannot start a transaction within a transaction`. It now waits.
- Fixed: a failed `COMMIT` left the transaction open, so every later `sql.begin()` failed.

#### `bun:sqlite`

- Fixed: `bun:sqlite` lost writes at process exit when `iterate()` on a statement that writes, like `INSERT ... RETURNING`, wasn't read to the end. They're now committed. An explicit `BEGIN` is still rolled back.
- Fixed: `new Database(path, { create: false })` failed with `SQLITE_MISUSE` on an existing file, and so did `{}`, `{ readonly: false }` and `{ readwrite: false }`.
- Fixed: `{ readonly: true, create: true }` opened the database read-write. It now throws a `TypeError`.
- Fixed: `db.run("a; b; c")` and `db.exec()` swallowed runtime errors in every statement except the last. They now throw at the failing statement and skip the rest.
- Fixed: after `ALTER TABLE`, an existing `Statement` returned rows with the old column names, or values under the wrong keys.
- Fixed: `{ name: 1 }` bound `NULL` to `$name` by default, and `{ $name: 1 }` threw with `strict: true`. Both forms now work in both modes.
- Fixed: `db.prepare(sql, "x")` ignored a binding that wasn't an array.
- Fixed: `row["1"]` was `undefined` for `SELECT 1`.
- Fixed: `changes` also counted rows written by triggers, foreign key cascades and FTS5 shadow tables. It now counts the statement's own rows, like `node:sqlite`.
- Fixed: `break` out of a `stmt.iterate()` loop left the cursor open, so a later `DROP TABLE` failed with `database table is locked`.
- Fixed: `db.transaction(fn)` committed when `fn` returned a promise that was already rejected, which is what an `async` function does when it throws before its first `await`. It now rolls back.
- Fixed: `stmt.run(new Date())`, or a `Map` or a `Promise` as the only argument, bound `NULL`. It now throws a `TypeError`.
- Fixed: terminating a Worker during a write with `RETURNING` outside a transaction could commit rows without their full-text (FTS) index entries. `MATCH` then missed those rows. `INSERT INTO ft(ft) VALUES('rebuild')` repairs the index.
- Fixed: `db.query()` cached SQL text of any size, so a loop over multi-megabyte queries with changing text grew to 953 MB. It now peaks at 92 MB.
- Improved: statements now report their native memory to the garbage collector. 20,000 dropped 200-parameter statements hold 27 MB instead of 465 MB.
- Improved: `stmt.get()` stops after the first row instead of reading the whole result. Statements that write (`INSERT`, `UPDATE`, `DELETE … RETURNING`) still apply and commit all their changes.
- Improved: rows with 63 or more columns take 310 instructions per cell, down from 1,665.
- Improved: `db.run()` with a string of 80,000 statements takes 0.11 s of CPU time, down from 5.34 s.
- Improved: Workers no longer share one SQLite malloc mutex. As a result, `PRAGMA hard_heap_limit` and `soft_heap_limit` are accepted but no longer enforced, like in `node:sqlite`.

#### `node:sqlite`

- Fixed: `db.close()` kept the file and its `-wal` and `-shm` files open until every `StatementSync` was garbage collected, which caused `EBUSY` on Windows.
- Fixed: a `Proxy` (Vue `reactive()`, MobX) or a module namespace as named parameters bound `NULL` to every parameter.
- Fixed: `stmt.get()` on `INSERT ... RETURNING` returned a row even though the commit failed on a deferred foreign key. It now throws.
- Improved: `stmt.get()` on a 5-column row takes 4,077 instructions, down from 13,458.

### TypeScript types

- Fixed: `toHaveBeenCalledOnce()` was missing from `bun-types`. The matcher already worked in `bun test`.

<!-- https://github.com/oven-sh/bun/commit/bfa1209e35663056248406661f1b90851e5c72fa -->

- Fixed: `@types/bun` typed `Bun.hash()` as `number | bigint`. It always returns a `bigint`.
- Fixed: `@types/bun` accepted a `Buffer` or `URL` as the path in `Bun.mmap()`. Both throw at runtime.
- Fixed: `@types/bun` was missing `byteLength` on instances of `Bun.MD5`, `Bun.SHA256` and the other hash classes.

<!-- https://github.com/oven-sh/bun/commit/d0831bc9fe53ceec29281fb9b7dfdebea39bc351 -->

- Fixed: `@types/bun` typed the `cpuTime` fields of a subprocess's `resourceUsage()` as `number` when they're `bigint`, and `FileSystemRouter`'s `origin` as `string` when it can be `null`.

<!-- https://github.com/oven-sh/bun/commit/3880151f0abacaf7dce9d6d0ab1c7be295d14bb4 -->

## Thanks to 39 contributors!

- [@0xh3x](https://github.com/0xh3x)
- [@1cedsoda](https://github.com/1cedsoda)
- [@alex-mextner](https://github.com/alex-mextner)
- [@alii](https://github.com/alii)
- [@Artoria2e5](https://github.com/Artoria2e5)
- [@bompus](https://github.com/bompus)
- [@C0casio45](https://github.com/C0casio45)
- [@cirospaciari](https://github.com/cirospaciari)
- [@davidhevn](https://github.com/davidhevn)
- [@deepshekhardas](https://github.com/deepshekhardas)
- [@Dex4Sure](https://github.com/Dex4Sure)
- [@dylan-conway](https://github.com/dylan-conway)
- [@EffortlessSteven](https://github.com/EffortlessSteven)
- [@eriklangille](https://github.com/eriklangille)
- [@filipesarturi](https://github.com/filipesarturi)
- [@fredluz](https://github.com/fredluz)
- [@gerardp](https://github.com/gerardp)
- [@gohryt](https://github.com/gohryt)
- [@himself65](https://github.com/himself65)
- [@hughescr](https://github.com/hughescr)
- [@Jarred-Sumner](https://github.com/Jarred-Sumner)
- [@jdjkelly](https://github.com/jdjkelly)
- [@jecquas](https://github.com/jecquas)
- [@jobansd](https://github.com/jobansd)
- [@kernoeb](https://github.com/kernoeb)
- [@KilianB](https://github.com/KilianB)
- [@MarshallOfSound](https://github.com/MarshallOfSound)
- [@Neekoras](https://github.com/Neekoras)
- [@nikeee](https://github.com/nikeee)
- [@nikelborm](https://github.com/nikelborm)
- [@ousamabenyounes](https://github.com/ousamabenyounes)
- [@pjb3](https://github.com/pjb3)
- [@RajeshKumar11](https://github.com/RajeshKumar11)
- [@ralyodio](https://github.com/ralyodio)
- [@robobun](https://github.com/robobun)
- [@RubenPari](https://github.com/RubenPari)
- [@sosukesuzuki](https://github.com/sosukesuzuki)
- [@steipete](https://github.com/steipete)
- [@SyMind](https://github.com/SyMind)
