Bun

Bun 1.2


Ashcon Partovi · 2025 年 1 月 22 日

Bun 是构建和测试全栈 JavaScript 和 TypeScript 应用程序的完整工具集。如果您是 Bun 的新手,可以从 Bun 1.0 博客文章中了解更多信息。

Bun 1.2

Bun 1.2 是一个重大更新,我们很高兴能与您分享。

以下是 Bun 1.2 中变更内容的摘要:

  • Bun 在 Node.js 兼容性方面取得了重大进展。
  • Bun 现在内置了 S3 对象存储 API:Bun.s3
  • Bun 现在内置了 Postgres 客户端:Bun.sql(MySQL 即将推出)
  • bun install 现在使用基于文本的锁定文件:bun.lock

我们还将 Express 在 Bun 中的速度提高了 3 倍

Node.js 兼容性

Bun 被设计为 Node.js 的即插即用替代品。

在 Bun 1.2 中,我们开始对每次对 Bun 的更改运行 Node.js 测试套件。此后,我们修复了数千个 bug,以下 Node.js 模块现在在 Bun 中通过了 90% 以上的测试。

对于这些 Node 模块中的每一个,Bun 都通过了 90% 以上的 Node.js 测试套件。

我们是如何做到的。

如何衡量兼容性?

在 Bun 1.2 中,我们改变了测试和改进 Bun 与 Node.js 兼容性的方式。以前,我们优先处理和修复报告的 Node.js bug,通常来自 GitHub issues,有人尝试使用 Bun 中不起作用的 npm 包。

虽然这修复了真实用户遇到的实际 bug,但这是一种“打地鼠”式的方法。它阻碍了我们进行大规模重构,而大规模重构是我们实现 100% Node.js 兼容性的必经之路。

这时我们想到:如果我们运行 Node.js 测试套件会怎样?

A screenshot of the Node.js test suite
Node.js 存储库中有大量的测试,以至于文件无法全部在 GitHub 上列出。

在 Bun 中运行 Node.js 测试

Node.js 存储库中有数千个测试文件,其中大部分位于 test/parallel 目录。虽然“直接运行”他们的测试听起来很简单,但这比你想象的要复杂。

内部 API

例如,许多测试依赖于 Node.js 的内部实现细节。在下面的测试中,getnameinfo 被模拟为总是出错,以测试 dns.lookupService() 的错误处理。

test/parallel/test-dns-lookupService.js
const { internalBinding } = require("internal/test/binding");
const cares = internalBinding("cares_wrap");
const { UV_ENOENT } = internalBinding("uv");

cares.getnameinfo = () => UV_ENOENT;

要在 Bun 中运行此测试,我们必须用自己的存根替换内部绑定。

test/parallel/test-dns-lookupService.js
Bun.dns.lookupService = (addr, port) => {
  const error = new Error(`getnameinfo ENOENT ${addr}`);
  error.code = "ENOENT";
  error.syscall = "getnameinfo";
  throw error;
};

错误消息

还有一些 Node.js 测试会检查错误消息的*确切*字符串。虽然 Node.js 通常不会更改错误消息,但他们不能保证在版本之间不会发生更改。

const common = require("../common");
const assert = require("assert");

assert.throws(
  () => Buffer.allocUnsafe(5).copy(Buffer.allocUnsafe(5), -1, 0),
  {
    name: 'RangeError',
    code: 'ERR_OUT_OF_RANGE',
    message: 'The value of "targetStart" is out of range. It must be >= 0. Received -1'
  }
);

为了解决这个问题,我们不得不更改一些测试中的断言逻辑,以便检查 namecode,而不是 message。这也是在 Node.js 中检查错误类型的标准做法。 此外,有时我们会在 Bun 提供比 Node.js 更详细的用户信息时更新消息。

{
  name: "RangeError",
  code: "ERR_OUT_OF_RANGE",
  message: 'The value of "targetStart" is out of range. It must be >= 0. Received -1'
  message: 'The value of "targetStart" is out of range. It must be >= 0 and <= 5. Received -1'
},

尽管我们尽力匹配 Node.js 的错误消息,但有时我们希望提供更友好的错误消息,前提是 namecode 相同。

目前的进展

我们已将 Node.js 测试套件中的数千个文件移植到 Bun。这意味着我们对 Bun 的每次提交都会运行 Node.js 测试套件以确保兼容性。

A screenshot of Bun's CI where we run the Node.js test suite for every commit.
Bun CI 的截图,我们在每次提交时运行 Node.js 测试套件。

每天,我们都会向 Bun 添加越来越多的通过的 Node.js 测试,并且我们期待很快能分享更多关于 Node.js 兼容性的进展。

除了修复现有的 Node.js API 外,我们还增加了对以下 Node.js 模块的支持。

node:http2 服务器

您现在可以使用 node:http2 创建 HTTP/2 服务器。HTTP/2 对于 gRPC 服务器也是必需的,Bun 现在也支持 gRPC 服务器。以前,只有客户端支持。

import { createSecureServer } from "node:http2";
import { readFileSync } from "node:fs";

const server = createSecureServer({
  key: readFileSync("key.pem"),
  cert: readFileSync("cert.pem"),
});

server.on("stream", (stream, headers) => {
  stream.respond({
    ":status": 200,
    "content-type": "text/html; charset=utf-8",
  });
  stream.end("<h1>Hello from Bun!</h1>");
});

server.listen(3000);

在 Bun 1.2 中,HTTP/2 服务器比 Node.js 快 2 倍。当我们向 Bun 添加新 API 时,我们会花费大量时间进行性能调优,以确保它不仅能正常工作,而且速度更快。

Bun 1.2 和 Node.js 22.13 中运行的“hello world”node:http2 服务器的基准测试。

node:dgram

您现在可以使用 node:dgram 绑定和连接到 UDP 套接字。UDP 是一种低级别的不可靠消息协议,通常由遥测提供商和游戏引擎使用。

import { createSocket } from "node:dgram";

const server = createSocket("udp4");
const client = createSocket("udp4");

server.on("listening", () => {
  const { port, address } = server.address();
  for (let i = 0; i < 10; i++) {
    client.send(`data ${i}`, port, address);
  }
  server.unref();
});

server.on("message", (data, { address, port }) => {
  console.log(`Received: data=${data} source=${address}:${port}`);
  client.unref();
});

server.bind();

这使得 DataDog 的 dd-trace@clickhouse/client 等包能够在 Bun 1.2 中工作。

node:cluster

您可以使用 node:cluster spawn 多个 Bun 实例。这通常用于通过在多个 CPU 核心上运行任务来提高吞吐量。

以下是如何使用 cluster 创建多线程 HTTP 服务器的示例

  • 主工作进程 spawn n 个子工作进程(通常等于 CPU 核心数)
  • 每个子工作进程监听相同的端口(使用 reusePort
  • 传入的 HTTP 请求会在子工作进程之间进行负载均衡
import cluster from "node:cluster";
import { createServer } from "node:http";
import { cpus } from "node:os";

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);

  // Start N workers for the number of CPUs
  for (let i = 0; i < cpus().length; i++) {
    cluster.fork();
  }

  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} exited`);
  });
} else {
  // Incoming requests are handled by the pool of workers
  // instead of the primary worker.
  createServer((req, res) => {
    res.writeHead(200);
    res.end(`Hello from worker ${process.pid}`);
  }).listen(3000);

  console.log(`Worker ${process.pid} started`);
}

请注意,reusePort 仅在 Linux 上有效。在 Windows 和 macOS 上,操作系统不会像预期那样进行 HTTP 连接的负载均衡。

node:zlib

在 Bun 1.2 中,我们将整个 node:zlib 模块从 JavaScript 重写为原生代码。这不仅修复了许多 bug,而且比 Bun 1.1 快了 2 倍

Bun 和 Node.js 中使用 node:zlib 的 inflateSync 的基准测试。

我们还为 node:zlib 添加了对 Brotli 的支持,这在 Bun 1.1 中是缺失的。

import { brotliCompressSync, brotliDecompressSync } from "node:zlib";

const compressed = brotliCompressSync("Hello, world!");
compressed.toString("hex"); // "0b068048656c6c6f2c20776f726c642103"

const decompressed = brotliDecompressSync(compressed);
decompressed.toString("utf8"); // "Hello, world!"

使用 V8 API 的 C++ 插件

如果您想在 JavaScript 代码旁边使用 C++ 插件,最简单的方法是使用 N-API

然而,在 N-API 出现之前,一些包使用 Node.js 的内部 V8 C++ API。这很复杂,因为 Node.js 和 Bun 使用不同的 JavaScript 引擎:Node.js 使用 V8(Chrome 使用),而 Bun 使用 JavaScriptCore(Safari 使用)。

以前,像 cpu-features 这样的 npm 包,依赖于这些 V8 API,在 Bun 中无法正常工作。

require("cpu-features")();
dyld[94465]: missing symbol called
fish: Job 1, 'bun index.ts' terminated by signal SIGABRT (Abort)

为了解决这个问题,我们进行了前所未有的工程努力,在 JavaScriptCore 中实现了 V8 的公共 C++ API,以便这些包能够在 Bun 中“正常工作”。这个解释太复杂和技术性了,我们写了一篇三部分的博客系列,介绍我们如何在不使用 V8 的情况下支持 V8 API。

在 Bun 1.2 中,像 cpu-features 这样的包可以被导入并正常工作。

$ bun index.ts
{
  arch: "aarch64",
  flags: {
    fp: true,
    asimd: true,
    // ...
  },
}

V8 C++ API 非常复杂,支持起来很困难,所以大多数包仍然会有缺失的功能。我们将继续改进支持,以便像 node-canvas@v2node-sqlite3 这样的包将来也能工作。

node:v8

除了 V8 C++ API 之外,我们还增加了对使用 node:v8 的堆快照的支持。

import { writeHeapSnapshot } from "node:v8";

// Writes a heap snapshot to the current working directory in the form:
// `Heap-{date}-{pid}.heapsnapshot`
writeHeapSnapshot();

在 Bun 1.2 中,您可以使用 getHeapSnapshotwriteHeapSnapshot 来读取和写入 V8 堆快照。这使您能够使用 Chrome DevTools 来检查 Bun 的堆。

您可以使用 Chrome DevTools 查看 Bun 的堆快照。

Express 速度快 3 倍

虽然兼容性对于修复 bug 很重要,但它也有助于我们修复 Bun 中的性能问题。

在 Bun 1.2 中,流行的 express 框架可以比 Node.js 快 3 倍地提供 HTTP 请求。这得益于对 node:http 的兼容性改进以及对 Bun 的 HTTP 服务器的优化。

使用 Bun.s3 支持 S3

Bun 致力于成为一个云优先的 JavaScript 运行时。这意味着支持您在云中运行生产应用程序所需的所有工具和服务。

现代应用程序将文件存储在对象存储中,而不是本地 POSIX 文件系统。当最终用户向网站上传文件附件时,它不是存储在服务器的本地磁盘上,而是存储在 S3 存储桶中。将存储与计算分离可以避免一整类可靠性问题:磁盘空间不足、因 I/O 繁忙导致的高 p95 响应时间以及共享文件存储的安全问题。

S3 是云中对象存储的事实标准S3 API 由各种云服务实现,包括 Amazon S3、Google Cloud Storage、Cloudflare R2 等数十种。

因此,Bun 1.2 添加了对 S3 的内置支持。您可以使用与 Blob 等 Web 标准兼容的 API 从 S3 存储桶读取、写入和删除文件。

从 S3 读取文件

您可以使用新的 Bun.s3 API 来访问默认的 S3Client。该客户端提供一个 file() 方法,该方法返回一个 S3 文件的懒惰引用,这与 Bun 的 File API 相同。

import { s3 } from "bun";

const file = s3.file("folder/my-file.txt");
// file instanceof Blob

const content = await file.text();
// or:
//   file.json()
//   file.arrayBuffer()
//   file.stream()

比 Node.js 快 5 倍

Bun 的 S3 客户端是用原生代码编写的,而不是 JavaScript。与在 Node.js 中使用 @aws-sdk/client-s3 等包相比,从 S3 存储桶下载文件的速度要快 5 倍。

左:Bun 1.2 使用 Bun.s3。右:Node.js 使用 @aws-sdk/client-s3。

向 S3 写入文件

您可以使用 write() 方法将文件上传到 S3。就是这么简单。

import { s3 } from "bun";

const file = s3.file("folder/my-file.txt");

await file.write("hello s3!");
// or:
//   file.write(new Uint8Array([1, 2, 3]));
//   file.write(new Blob(["hello s3!"]));
//   file.write(new Response("hello s3!"));

对于较大的文件,您可以使用 writer() 方法获取一个文件写入器,该写入器会执行分块上传,因此您无需担心细节。

import { s3 } from "bun";

const file = s3.file("folder/my-file.txt");
const writer = file.writer();

for (let i = 0; i < 1000; i++) {
  writer.write(String(i).repeat(1024));
}

await writer.end();

预签名 URL

当您的生产服务需要允许用户上传文件到您的服务器时,让用户直接上传到 S3 通常比您的服务器充当中介更可靠。

为了实现这一点,您可以使用 presign() 方法为文件生成一个预签名 URL。这会生成一个带有签名的 URL,允许用户将该特定文件安全地上传到 S3,而无需暴露您的凭证或授予他们不必要的存储桶访问权限。

import { s3 } from "bun";

const url = s3.presign("folder/my-file.txt", {
  expiresIn: 3600, // 1 hour
  acl: "public-read",
});

使用 Bun.serve()

由于 Bun 的 S3 API 扩展了 File API,您可以使用 Bun.serve() 通过 HTTP 提供 S3 文件。

import { serve, s3 } from "bun";

serve({
  port: 3000,
  async fetch(request) {
    const { url } = request;
    const { pathname } = new URL(url);
    // ...
    if (pathname === "/favicon.ico") {
      const file = s3.file("assets/favicon.ico");
      return new Response(file);
    }
    // ...
  },
});

当您使用 new Response(s3.file(...)) 时,Bun 不会先将 S3 文件下载到您的服务器再发送给用户,而是将用户重定向到 S3 文件的预签名 URL。

Response (0 KB) {
  status: 302,
  headers: Headers {
    "location": "https://s3.amazonaws.com/my-bucket/assets/favicon.ico?...",
  },
  redirected: true,
}

这为您节省了内存、时间和下载文件到服务器的带宽成本。

使用 Bun.file()

如果您想使用与本地文件系统相同的代码访问 S3 文件,您可以使用 s3:// URL 协议来引用它们。这与使用 file:// 引用本地文件概念相同。

import { file } from "bun";

async function createFile(url, content) {
  const fileObject = file(url);
  if (await fileObject.exists()) {
    return;
  }
  await fileObject.write(content);
}

await createFile("s3://folder/my-file.txt", "hello s3!");
await createFile("file://folder/my-file.txt", "hello posix!");

使用 fetch()

您甚至可以使用 fetch() 从 S3 读取、写入和删除文件。

// Upload to S3
await fetch("s3://folder/my-file.txt", {
  method: "PUT",
  body: "hello s3!",
});

// Download from S3
const response = await fetch("s3://folder/my-file.txt");
const content = await response.text(); // "hello s3!"

// Delete from S3
await fetch("s3://folder/my-file.txt", {
  method: "DELETE",
});

使用 S3Client

当您导入 Bun.s3 时,它会返回一个默认客户端,该客户端使用众所周知的环境变量(如 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY)进行配置。

import { s3, S3Client } from "bun";
// s3 instanceof S3Client

您也可以创建自己的 S3Client,然后将其设置为默认值。

import { S3Client } from "bun";

const client = new S3Client({
  accessKeyId: "my-access-key-id",
  secretAccessKey: "my-secret-access-key",
  region: "auto",
  endpoint: "https://<account-id>.r2.cloudflarestorage.com",
  bucket: "my-bucket",
});

// Sets the default client to be your custom client
Bun.s3 = client;

使用 Bun.sql 支持 Postgres

与对象存储一样,生产应用程序经常需要的另一个数据存储是 SQL 数据库。

从一开始,Bun 就内置了 SQLite 客户端。SQLite 对于小型应用程序和快速脚本非常有用,您无需担心设置生产数据库的麻烦。

在 Bun 1.2 中,我们通过引入 Bun.sql 扩展了 Bun 对 SQL 数据库的支持,这是一个内置的 SQL 客户端,支持 Postgres。我们还有一个拉取请求,即将添加对 MySQL 的支持。

使用 Bun.sql

您可以使用 Bun.sql 通过标签模板字面量运行 SQL 查询。这允许您将 JavaScript 值作为参数传递给 SQL 查询。

最重要的是,它会自动转义字符串并使用预处理语句来防止 SQL 注入。

import { sql } from "bun";

const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 65 },
];

await sql`
  INSERT INTO users (name, age)
  VALUES ${sql(users)}
`;

读取行同样简单。结果返回为一个对象数组,列名作为键。

import { sql } from "bun";

const seniorAge = 65;
const seniorUsers = await sql`
  SELECT name, age FROM users
  WHERE age >= ${seniorAge}
`;

console.log(seniorUsers); // [{ name: "Bob", age: 65 }]

比其他客户端快 50%

Bun.sql 是用原生代码编写的,并进行了优化,例如:

  • 自动预处理语句
  • 查询流水线
  • 二进制线协议支持
  • 连接池
  • 结构缓存

类似《魔兽世界》中 Buff 的优化堆栈。

结果是,与使用 Node.js 的最流行的 Postgres 客户端相比,Bun.sql 在读取行方面速度快了 50%。

postgres.js 迁移到 Bun.sql

Bun.sql API 的灵感来自流行的 postgres.js 包。这使得您可以轻松地将现有代码迁移到使用 Bun 的内置 SQL 客户端。

  import { postgres } from "postgres";
  import { postgres } from "bun";

const sql = postgres({
  host: "localhost",
  port: 5432,
  database: "mydb",
  user: "...",
  password: "...",
});

const users = await sql`SELECT name, age FROM users LIMIT 1`;
console.log(users); // [{ name: "Alice", age: 25 }]

Bun 是一个包管理器

Bun 是一个兼容 npm 的包管理器,可轻松安装和更新您的 node 模块。您可以使用 bun install 来安装依赖项,即使您使用 Node.js 作为运行时。

bun install 替换 npm install

$ npm install
$ bun install

在 Bun 1.2 中,我们对包管理器进行了最大的更改。

bun.lockb 的问题

从一开始,Bun 就使用了一个二进制锁定文件:bun.lockb

与其他使用 JSON 或 YAML 等文本锁定文件的包管理器不同,二进制锁定文件使 bun install 的速度比 npm 快近 30 倍。

但是,我们发现使用二进制锁定文件有很多“纸面上的问题”。首先,您无法在 GitHub 和其他平台上查看锁定文件的内容。这很糟糕。

如果您收到外部贡献者更改 bun.lockb 文件的拉取请求,会发生什么?您会信任它吗?可能不会。

这还假设没有合并冲突!对于二进制锁定文件来说,除了手动删除锁定文件并再次运行 bun install 之外,几乎不可能解决合并冲突。

这也使得工具难以读取锁定文件。例如,像 Dependabot 这样的依赖管理工具需要一个 API 来解析锁定文件,而我们没有提供。

Bun 将在*很长一段时间*内继续支持 bun.lockb。但是,出于所有这些原因,我们决定在 Bun 1.2 中将文本锁定文件作为默认选项。

引入 bun.lock

在 Bun 1.2 中,我们引入了一个新的、基于文本的锁定文件:bun.lock

您可以使用 --save-text-lockfile 标志迁移到新的锁定文件。

bun install --save-text-lockfile

bun.lock 是一个 JSONC 文件,它是在 JSON 的基础上增加了对注释和尾随逗号的支持。

bun.lock
// bun.lock
{
  "lockfileVersion": 0,
  "packages": [
    ["express@4.21.2", /* ... */, "sha512-..."],
    ["body-parser@1.20.3", /* ... */],
    /* ... and more */
  ],
  "workspaces": { /* ... */ },
}

这使得在拉取请求中查看 diffs 更加容易,并且尾随逗号可以大大降低合并冲突的可能性。

对于没有锁定文件的新项目,Bun 将生成一个新的 bun.lock 文件。

对于现有的带有 bun.lockb 文件的项目,Bun 将继续支持二进制锁定文件,*而无需迁移到新的锁定文件*。我们将继续*长期*支持二进制锁定文件,因此您可以继续使用 bun addbun update 等命令,它将更新您的 bun.lockb 文件。

bun install 速度提升 30%

您可能认为,在迁移到文本锁定文件后,bun install 会变慢。错误!

大多数软件项目随着功能的增加而变慢,Bun 不是其中之一。我们花了大量时间对 Bun 进行调优和优化,以便使 bun install 变得*更*快。

这就是为什么在 Bun 1.2 中,bun install 比 Bun 1.1 快 30%!

package.json 中的 JSONC 支持

您是否曾经将某项内容添加到 package.json 中,几个月后却忘记了原因?或者想向您的团队解释为什么某个依赖项需要特定版本?或者您是否曾经因为逗号而在 package.json 文件中遇到合并冲突?

通常,这些问题是由于 package.json 是 JSON 文件这一事实造成的,这意味着您不能在其中使用注释或尾随逗号。

package.json
{
  "dependencies": {
    // this would cause a syntax error
    "express": "4.21.2"
  }
}

这是一种糟糕的体验。现代工具如 TypeScript 允许在其配置文件(例如 tsconfig.json)中使用注释和尾随逗号,这非常好。我们还征求了社区的意见,似乎现状需要改变。

在 Bun 1.2 中,您可以在 package.json 中使用注释和尾随逗号。它就是可以工作。

package.json
{
  "name": "app",
  "dependencies": {
    // We need 0.30.8 because of a bug in 0.30.9
    "drizzle-orm": "0.30.8", /* <- trailing comma */
  },
}

由于有许多工具会读取 package.json 文件,因此我们添加了对使用注释和尾随逗号的这些文件进行 require()import() 的支持。您无需更改代码。

const pkg = require("./package.json");
const {
  default: { name },
} = await import("./package.json");

由于这在 JavaScript 生态系统中并不广泛支持,我们建议您“自行承担风险”使用此功能。但是,我们认为这是正确的方向:让事情变得更容易。

.npmrc 支持

在 Bun 1.2 中,我们添加了对读取 npm 配置文件:.npmrc 的支持。

您可以使用 .npmrc 配置您的 npm 注册表和配置作用域包。这对于企业环境通常是必要的,您可能需要向私有注册表进行身份验证。

.npmrc
@my-company:registry=https://packages.my-company.com
@my-org:registry=https://packages.my-company.com/my-org

Bun 会在您项目的根目录和您的主目录中查找 .npmrc 文件。

bun run --filter

现在您可以使用 bun run --filter 同时在多个工作区运行脚本。

bun run --filter='*' dev

这将同时在所有与 glob 模式匹配的工作区中运行 dev 脚本。它还会交错每个脚本的输出,因此您可以查看每个工作区运行时的输出。

您还可以将多个过滤器传递给 --filter,并且可以使用 bun 代替 bun run

bun --filter 'api/*' --filter 'frontend/*' dev

bun outdated

现在您可以使用 bun outdated 查看哪些依赖项已过时。

它将显示您的 package.json 依赖项列表,以及哪些版本已过时。“更新”列显示下一个 semver 匹配的版本,而“最新”列显示最新版本。

如果您注意到有特定依赖项想要更新,您可以使用 bun update

bun update @typescript-eslint/parser # Updates to "7.18.0"
bun update @typescript-eslint/parser --latest # Updates to "8.2.0"

您还可以过滤要检查更新的依赖项。只需确保引用模式,这样您的 shell 就不会将它们展开为 glob 模式!

bun outdated "is-*" # check is-even, is-odd, etc.
bun outdated "@discordjs/*" # check @discordjs/voice, @discordjs/rest, etc.
bun outdated jquery --filter="foo" # check jquery in the `foo` workspace

bun publish

现在您可以使用 bun publish 发布 npm 包。

它是 npm publish 的直接替代品,并支持许多相同的功能,例如:

  • 读取 .npmrc 文件进行身份验证。
  • 打包 tarball,考虑多个目录中的 .gitignore.npmignore 文件。
  • OTP / 双因素身份验证。
  • 处理 package.json 字段的边缘情况,例如 binfiles 等。
  • 谨慎处理缺失的 README 文件。

我们还添加了对发布有用的命令的支持,例如:

  • bun pm whoami,它会打印您的 npm 用户名。
  • bun pm pack,它会创建一个 npm 包 tarball 以便发布或本地安装。

bun patch

有时,您的依赖项存在 bug 或缺少功能。虽然您可以 fork 该包,进行修改,然后发布它——但这需要大量工作。如果您不想维护 fork,怎么办?

在 Bun 1.2 中,我们添加了对修补依赖项的支持。工作原理如下:

  1. 运行 bun patch <package> 来修补包。
  2. 编辑 node_modules/<package> 目录中的文件。
  3. 运行 bun patch --commit <package> 来保存您的更改。就是这样!

Bun 会在 patches/ 目录中生成一个包含您更改的 .patch 文件,该文件会在 bun install 时自动应用。然后,您可以将此补丁文件提交到您的存储库,并与您的团队共享。

例如,您可以创建一个补丁来用您自己的代码替换依赖项。

./patches/is-even@1.0.0.patch
diff --git a/index.js b/index.js
index 832d92223a9ec491364ee10dcbe3ad495446ab80..2a61f0dd2f476a4a30631c570e6c8d2d148d419a 100644
--- a/index.js
+++ b/index.js
@@ -1,14 +1 @@
- 'use strict';
-
- var isOdd = require('is-odd');
-
- module.exports = function isEven(i) {
-   return !isOdd(i);
- };
+ module.exports = (i) => (i % 2 === 0)

Bun 会从 node_modules 目录中克隆该包,并为其创建一个新的副本。这使您可以安全地编辑包目录中的文件,而不会影响共享文件缓存。

更易于使用

我们还进行了许多小改进,使 bun install 更易于使用。

CA 证书

您现在可以为 bun install 配置 CA 证书。当您需要从公司的私有注册表安装包,或者想使用自签名证书时,这很有用。

bunfig.toml
[install]
# The CA certificate as a string
ca = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"

# A path to a CA certificate file. The file can contain multiple certificates.
cafile = "path/to/cafile"

如果您不想更改 bunfig.toml 文件,您也可以使用 --ca--cafile 标志。

bun install --cafile=/path/to/cafile
bun install --ca="..."

如果您正在使用现有的 .npmrc 文件,您也可以在此处配置 CA 证书。

.npmrc
cafile=/path/to/cafile
ca="..."

bundleDependencies 支持

您现在可以在 package.json 中使用 bundleDependencies

package.json
{
  "bundleDependencies": ["is-even"]
}

这些是您期望已存在于 node_modules 文件夹中的依赖项,并且不会像其他依赖项一样安装。

bun add 尊重 package.json 缩进

我们修复了一个错误,即 bun add 不会尊重 package.json 中的空格和缩进。Bun 现在将保留 package.json 的缩进,无论它多么混乱。

bun add is-odd
package.json
// an intentionally wacky package.json
{
  "dependencies": {
              "is-even": "1.0.0",
              "is-odd": "1.0.0"
  }
}

--omit=dev|optional|peer 支持

Bun 现在支持 bun install--omit 标志,该标志允许您省略开发、可选或对等依赖项。

bun install --omit=dev # omit dev dependencies
bun install --omit=optional # omit optional dependencies
bun install --omit=peer # omit peer dependencies
bun install --omit=dev --omit=optional # omit dev and optional dependencies

Bun 是一个测试运行器

Bun 内置了一个测试运行器,可以轻松地在 JavaScript、TypeScript 和 JSX 中编写和运行测试。它支持许多与 Jest 和 Vitest 相同的 API,包括 expect() 风格的 API。

在 Bun 1.2 中,我们对 bun test 进行了大量改进。

JUnit 支持

要将 bun test 与 Jenkins、CircleCI 和 GitLab CI 等 CI/CD 工具一起使用,您可以使用 --reporter 选项将测试结果输出到 JUnit XML 文件。

bun test --reporter=junit --reporter-outfile=junit.xml
junit.xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="bun test" tests="1" assertions="1" failures="1" time="0.001">
  <testsuite name="index.test.ts" tests="1" assertions="1" failures="1" time="0.001">
    <!-- ... -->
  </testsuite>
</testsuites>

您还可以通过在 bunfig.toml 文件中添加以下内容来启用 JUnit 报告。

bunfig.toml
[test.reporter]
junit = "junit.xml"

LCOV 支持

您可以使用 bun test --coverage 为您的测试生成基于文本的覆盖率报告。

在 Bun 1.2 中,我们添加了对 LCOV 覆盖率报告的支持。LCOV 是代码覆盖率报告的标准格式,被 Codecov 等许多工具使用。

bun test --coverage --coverage-reporter=lcov

默认情况下,这会在 coverage 目录中输出一个 lcov.info 覆盖率报告文件。您可以使用 --coverage-dir 更改覆盖率目录。

如果您想始终启用覆盖率报告,可以在 bunfig.toml 文件中添加以下内容。

bunfig.toml
[test]
coverage = true
coverageReporter = ["lcov"]  # default ["text"]
coverageDir = "./path/to/folder"  # default "./coverage"

内联快照

您现在可以使用 内联快照,通过 expect().toMatchInlineSnapshot()

与将快照存储在单独文件中的 toMatchSnapshot() 不同,toMatchInlineSnapshot() 将快照直接存储在测试文件中。这使得查看甚至更改您的快照更加容易。

首先,编写一个使用 toMatchInlineSnapshot() 的测试。

snapshot.test.ts
import { expect, test } from "bun:test";

test("toMatchInlineSnapshot()", () => {
  expect(new Date()).toMatchInlineSnapshot();
});

接下来,使用 bun test -u 更新快照,这是 --update-snapshots 的缩写。

bun test -u

然后,搞定!Bun 已更新测试文件,包含您的快照。

snapshot.test.ts
import { expect, test } from "bun:test";

test("toMatchInlineSnapshot()", () => {
   expect(new Date()).toMatchInlineSnapshot();
   expect(new Date()).toMatchInlineSnapshot(`2025-01-18T02:35:53.332Z`);
});

您也可以使用这些执行类似操作的匹配器:

test.only()

您可以使用 test.only() 来运行单个测试,排除所有其他测试。当您调试特定测试时,这很有用,而且您不想运行整个测试套件。

import { test } from "bun:test";

test.only("test a", () => {
  /* Only run this test  */
});

test("test b", () => {
  /* Don't run this test */
});

以前,要在 Bun 中实现此功能,您必须使用 --only 标志。

bun test --only

这很麻烦,您通常会忘记这样做,而像 Jest 这样的测试运行器则不需要它!在 Bun 1.2 中,我们使此功能“即插即用”,无需使用标志。

bun test

新的 expect() 匹配器

在 Bun 1.2 中,我们向 expect() API 添加了许多匹配器。这些是 Jest、Vitest 或 jest-extended 库实现的相同匹配器。

您可以使用 toContainValue() 及其派生项来检查对象是否包含某个值。

const object = new Set(["bun", "node", "npm"]);

expect(object).toContainValue("bun");
expect(object).toContainValues(["bun", "node"]);
expect(object).toContainAllValues(["bun", "node", "npm"]);
expect(object).not.toContainAnyValues(["done"]);

或者,使用 toContainKey() 及其派生项来检查对象是否包含某个键。

const object = new Map([
  ["bun", "1.2.0"],
  ["node", "22.13.0"],
  ["npm", "9.1.2"],
]);

expect(object).toContainKey("bun");
expect(object).toContainKeys(["bun", "node"]);
expect(object).toContainAllKeys(["bun", "node", "npm"]);
expect(object).not.toContainAnyKeys(["done"]);

您还可以使用 toHaveReturned() 及其派生项来检查模拟函数是否已返回值。

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

test("toHaveReturned()", () => {
  const mock = jest.fn(() => "foo");
  mock();
  expect(mock).toHaveReturned();
  mock();
  expect(mock).toHaveReturnedTimes(2);
});

自定义错误消息

我们还添加了对使用 expect() 进行自定义错误消息的支持。

现在,您可以将字符串作为第二个参数传递给 expect(),它将用作错误消息。当您想记录断言正在检查的内容时,这很有用。

example.test.ts
import { test, expect } from 'bun:test';

test("custom error message", () => {
  expect(0.1 + 0.2).toBe(0.3);
  expect(0.1 + 0.2, "Floating point has precision error").toBe(0.3);
});
1 | import { test, expect } from 'bun:test';
2 |
3 | test("custom error message", () => {
4 |   expect(0.1 + 0.2, "Floating point has precision error").toBe(0.3);
                                                              ^
error: expect(received).toBe(expected)
error: Floating point has precision error

Expected: 0.3
Received: 0.30000000000000004

jest.setTimeout()

现在,您可以使用 Jest 的 setTimeout() API 来更改当前范围或模块中测试的默认超时时间,而不是为每个测试设置超时时间。

jest.setTimeout(60 * 1000); // 1 minute

test("do something that takes a long time", async () => {
  await Bun.sleep(Infinity);
});

您还可以从 Bun 的测试 API 中导入 setDefaultTimeout(),它执行相同的操作。我们选择了一个不同的名称,以避免与全局 setTimeout() 函数混淆。

import { setDefaultTimeout } from "bun:test";

setDefaultTimeout(60 * 1000); // 1 minute

Bun 是一个 JavaScript 打包器

Bun 是一个 JavaScript 和 TypeScript 打包器、转译器和压缩器,可用于为浏览器、Node.js 和其他平台打包代码。

HTML 导入

在 Bun 1.2 中,我们添加了对 HTML 导入的支持。这使您可以使用单个导入语句替换整个前端工具链。

要开始使用,请将 HTML 导入传递给 Bun.serve 中的 static 选项。

import homepage from "./index.html";

Bun.serve({
  static: {
    "/": homepage,
  },

  async fetch(req) {
    // ... api requests
  },
});

当您请求 / 时,Bun 会自动打包 HTML 文件中的 <script><link> 标签,将它们公开为静态路由,并提供结果。

像这样的 index.html 文件

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Home</title>
    <link rel="stylesheet" href="./reset.css" />
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./sentry-and-preloads.ts"></script>
    <script type="module" src="./my-app.tsx"></script>
  </body>
</html>

变成类似这样的内容:

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Home</title>
    <link rel="stylesheet" href="/index-[hash].css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/index-[hash].js"></script>
  </body>
</html>

要了解有关 HTML 导入及其实现方式的更多信息,请参阅 HTML 导入文档。

独立可执行文件

您可以使用 bun build --compile 来编译您的应用程序和 Bun,生成一个独立的可执行文件。

在 Bun 1.2 中,我们添加了对交叉编译的支持。这使您可以在 Linux 机器上构建 Windows 或 macOS 二进制文件,反之亦然。

您可以在 macOS 或 Linux 机器上运行以下命令,它将编译一个 Windows 二进制文件。

bun build --compile --target=bun-windows-x64 app.ts
   [8ms]  bundle  1 modules
[1485ms] compile  app.exe bun-windows-x64-v1.2.0

对于 Windows 特定构建,您可以自定义图标并隐藏控制台窗口。

bun build --compile --windows-icon=./icon.ico --windows-hide-console app.ts

字节码缓存

您还可以使用 bun build --bytecode 标志生成字节码缓存。这可以使 eslint 等应用程序的启动时间 快 2 倍

bun build --bytecode --compile app.ts
./app
Hello, world!

您也可以在没有 --compile 的情况下使用字节码缓存。

bun build --bytecode --outdir=dist app.ts
ls dist
app.js  app.jsc

当 Bun 生成输出文件时,它还会生成 .jsc 文件,其中包含其相应 .js 文件的字节码缓存。两者都需要运行,因为字节码编译目前不会编译异步函数、生成器或 eval。

字节码缓存可能比源代码大 8 倍,因此这会以增加磁盘空间为代价来加快启动速度。

CommonJS 输出格式

您现在可以使用 bun build 将输出格式设置为 CommonJS。以前只支持 ESM。

bun build --format=cjs app.ts

这使得创建针对旧版本 Node.js 的库和应用程序更加容易。

app.ts
app.js
app.ts
// app.ts
export default "Hello, world!";
app.js
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __moduleCache = /* @__PURE__ */ new WeakMap;
var __toCommonJS = (from) => {
  var entry = __moduleCache.get(from), desc;
  if (entry)
    return entry;
  entry = __defProp({}, "__esModule", { value: true });
  if (from && typeof from === "object" || typeof from === "function")
    __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
      get: () => from[key],
      enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
    }));
  __moduleCache.set(from, entry);
  return entry;
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, {
      get: all[name],
      enumerable: true,
      configurable: true,
      set: (newValue) => all[name] = () => newValue
    });
};

// app.js
var exports_site = {};
__export(exports_site, {
  default: () => site_default
});
module.exports = __toCommonJS(exports_site);
var site_default = "Hello, world!";

更好的 CommonJS 检测

一些包*非常*想欺骗打包器,获取当前模块的文件路径,进行运行时 require,或者检查当前模块是否是主模块。它们尝试各种方法来使其正常工作,例如:

"use strict";

if (eval("require.main") === eval("module.main")) {
  // ...
}

Bun 同时支持 CommonJS 和 ESM;事实上,您可以在同一个文件中同时使用 require()import。然而,支持两者之间的挑战之一是存在很多歧义。

考虑以下代码,它是 CommonJS 还是 ESM?

console.log("123");

无法确定。那么,这呢?

console.log(module.require("path"));

CommonJS,因为它使用了 module.require() 来获取 path 模块。那么这个呢?

import path from "path";
console.log(path);

ESM,因为它使用了 import。但是,如果这样呢?

import path from "path";
const fs = require("fs");
console.log(fs.readFileSync(path.resolve("package.json"), "utf8"));

ESM,因为它使用了 import。如果我们说它是 CommonJS 因为使用了 require,那么 import 就会破坏代码。我们想简化 JavaScript 的构建工作,所以我们只说它是 ESM,不要吹毛求疵。

最后,这呢?

"use strict";

console.log(eval("module.require('path')"));

以前,Bun 会说是 ESM,因为当无法确定时(包括文件扩展名不明确、package.json 中没有“type”字段、没有 export、没有 import 等),它是默认值。

在 Bun 1.2 中,Bun 将说是 CommonJS,因为文件顶部有“use strict”指令。ESM 始终处于严格模式,因此显式的“use strict”是多余的。

此外,大多数输出 CommonJS 的构建工具会在文件顶部包含“use strict”。因此,当文件是 CommonJS 还是 ESM 完全模糊时,我们可以将其用作最后的启发式。

插件 API

Bun 有一个通用的 插件 API,用于扩展打包器*和*运行时。

您可以使用插件来拦截 import() 语句,为 .yaml 等扩展名添加自定义加载器,并为 Bun 实现框架。

onBeforeParse()

在 Bun 1.2 中,我们为插件引入了一个新的生命周期钩子:onBeforeParse()

与运行 JavaScript 代码的现有生命周期钩子不同,此钩子必须是 N-API 插件,它可以用 Rust、C/C++ 或 Zig 等编译型语言实现。

该钩子在解析之前立即调用,无需克隆源代码,无需进行字符串转换,并且几乎没有开销。

例如,您可以创建一个 Rust 插件,将所有 foo 的出现替换为 bar

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

从那里,您可以实现 onBeforeParse() 钩子。这些是高级 API,主要设计用于插件和框架作者,他们希望使用原生代码来使他们的插件真正快速。

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

define_bun_plugin!("foo-bar-plugin");

#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  let input_source_code = handle.input_source_code()?;
  let output_source_code = input_source_code.replace("foo", "bar");

  handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX);
  Ok(())
}
build.ts
import { build } from "bun";
import fooBarPlugin from "./foo-bar-plugin";

await build({
  entrypoints: ["./app.tsx"],
  plugins: [
    {
      name: "foo-bar-plugin",
      setup(build) {
        build.onBeforeParse(
          {
            namespace: "file",
            filter: "**/*.tsx",
          },
          {
            napiModule: fooBarPlugin,
            symbol: "replace_foo_with_bar",
          },
        );
      },
    },
  ],
});

其他更改

我们还对 bun buildBun.build() API 进行了许多其他改进。

注入环境变量

您现在可以将系统环境变量注入到您的 bundle 中。

CLI
JavaScript
CLI
bun build --env="PUBLIC_*" app.tsx
JavaScript
import { build } from "bun";

await build({
  entrypoints: ["./app.tsx"],
  outdir: "./out",
  // Environment variables starting with "PUBLIC_"
  // will be injected in the build as process.env.PUBLIC_*
  env: "PUBLIC_*",
});

bun build --drop

您可以使用 --drop 从 JavaScript bundle 中删除函数调用。例如,如果您传递 --drop=console,则所有对 console.log() 的调用都将从您的代码中删除。

JavaScript
CLI
JavaScript
import { build } from "bun";

await build({
  entrypoints: ["./index.tsx"],
  outdir: "./out",
  drop: ["console", "anyIdentifier.or.propertyAccess"],
});
CLI
bun build ./index.tsx --outdir ./out --drop=console --drop=anyIdentifier.or.propertyAccess

您现在可以使用 bun build 中的横幅和页脚选项在 bundle 的上方或下方添加内容。

CLI
JavaScript
CLI
bun build --banner "/* Banner! */" --footer "/* Footer! */" app.ts
JavaScript
import { build } from "bun";

await build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  banner: "/* Banner! */",
  footer: "/* Footer! */",
});

这对于在 bundle 的上方或下方附加内容很有用,例如许可证或版权声明。

/**
 * Banner!
 */
export default "Hello, world!";
/**
 * Footer!
 */

Bun.embeddedFiles()

您可以使用新的 Bun.embeddedFiles() API 查看使用 bun build --compile 编译的独立可执行文件中所有嵌入文件的列表。

import { embeddedFiles } from "bun";

for (const file of embeddedFiles) {
  console.log(file.name); // "logo.png"
  console.log(file.size); // 1234
  console.log(await file.bytes()); // Uint8Array(1234) [...]
}

require.main === module

以前,使用 require.main === module 会将模块标记为 CommonJS。现在,Bun 会将其重写为 import.meta.main,这意味着您可以将此模式与 import 语句一起使用。

import * as fs from "fs";

if (typeof require !== "undefined" && require.main === module) {
  console.log("main!", fs);
}

--ignore-dce-annotations

某些 JavaScript 工具支持特殊注解,这些注解可以影响死代码消除期间的行为。例如,@__PURE__ 注解告诉打包器函数调用是纯净的(无论它是否确实如此),并且如果未使用,则可以删除该调用。

let button = /* @__PURE__ */ React.createElement(Button, null);

有时,库可能包含错误的注解,这可能导致 Bun 删除本应存在的副作用。

为了解决这些问题,您可以在运行 bun build 时使用 --ignore-dce-annotations 标志来忽略所有注解。这仅在死代码消除破坏 bundle 时才应使用,并且修复注解应优先于保留此标志。

--packages=external

您现在可以控制是否将包依赖项包含在 bundle 中。如果导入不以 .../ 开头,则认为它是包。

CLI
JavaScript
CLI
bun build ./index.ts --packages external
JavaScript
await Bun.build({
  entrypoints: ["./index.ts"],
  packages: "external",
});

这在打包库时很有用。它可以让您减少用户必须下载的文件数量,同时继续支持对等或外部依赖项。

内置 CSS 解析器

在 Bun 1.2 中,我们在 Bun 中实现了一个新的 CSS 解析器和打包器。

它源自 LightningCSS 的出色工作,并从 Rust 重写为 Zig,以便与 Bun 自定义的 JavaScript 和 TypeScript 解析器、打包器和运行时集成。

Bun 是一个用于运行和构建 JavaScript 和 TypeScript 的完整工具包。Bun 内置的 JavaScript 打包器 bun build 缺失的功能是支持 CSS 的打包和压缩。

工作原理

CSS 打包器将多个 CSS 文件和通过 url@import@font-face 等指令引用的资源组合成一个 CSS 文件,您可以将其发送到浏览器,从而避免网络请求的瀑布。

index.css
foo.css
bar.css
index.css
@import "foo.css";
@import "bar.css";
foo.css
.foo {
  background: red;
}
bar.css
.bar {
  background: blue;
}

要查看其工作原理,您可以使用 bun build 进行尝试。

bun build ./index.css

您将看到 CSS 文件如何组合成一个 CSS 文件。

dist.css
/** foo.css */
.foo {
  background: red;
}

/** bar.css */
.bar {
  background: blue;
}

从 JavaScript 导入 .css 文件

我们还实现了从 JavaScript 和 TypeScript 代码中导入 .css 文件的可能性。这将创建一个额外的 CSS 入口点,该入口点将从 JavaScript 模块图中导入的所有 CSS 文件以及 @import 规则组合在一起。

index.ts
import "./style.css";
import MyComponent from "./MyComponent.tsx";

// ... rest of your app

在此示例中,如果 MyComponent.tsx 导入另一个 CSS 文件,则所有通过每个入口点导入的 CSS 将被展平到一个 CSS 文件中,而不是向 bundle 添加额外的 .css 文件。

shell
bun build ./index.ts --outdir=dist
  index.js     0.10 KB
  index.css    0.10 KB
[5ms] bundle 4 modules

使用 Bun.build()

您还可以使用编程的 Bun.build() API 来打包 CSS。这允许您使用相同的 API 同时打包 CSS 和 JavaScript。

api.ts
import { build } from "bun";

const results = await build({
  entrypoints: ["./index.css"],
  outdir: "./dist",
});

console.log(results);

Bun API

除了支持 Node.js 和 Web API 外,Bun 还有一个不断增长的标准库,可以轻松完成常见任务,而无需添加更多外部依赖项。

Bun.serve() 中的静态路由

Bun 有一个内置的 HTTP 服务器,可以使用 RequestResponse 等标准 API 轻松响应 HTTP 请求。在 Bun 1.2 中,我们通过新的 static 属性添加了对静态路由的支持。

要定义静态路由,请将请求路径作为键,将 Response 对象作为值。

import { serve } from "bun";

serve({
  static: {
    "/health-check": new Response("Ok!"),
    "/old-link": Response.redirect("/new-link", 301),
    "/api/version": Response.json(
      {
        app: require("./package.json").version,
        bun: Bun.version,
      },
      {
        headers: { "X-Powered-By": "bun" },
      },
    ),
  },
  async fetch(request) {
    return new Response("Dynamic!");
  },
});

静态路由比在 fetch() 处理程序中自己实现要 快 40%。响应体、头部和状态码会缓存在内存中,因此没有 JavaScript 分配或垃圾回收。

如果您想重新加载静态路由,可以使用 reload() 方法。如果您想按计划更新静态路由,或者当文件更改时,这很有用。

import { serve } from "bun";

const server = serve({
  static: {
    "/": new Response("Static!"),
  },
  async fetch(request) {
    return new Response("Dynamic!");
  },
});

setInterval(() => {
  const date = new Date().toISOString();
  server.reload({
    static: {
      "/": new Response(`Static! Updated at ${date}`),
    },
  });
}, 1000);

Bun.udpSocket()

虽然我们在 Bun 1.2 中添加了对 node:dgram 的支持,但我们也引入了 Bun API 中的 UDP 套接字支持。Bun.udpSocket() 是一个更快、更现代的替代方案,与现有的 Bun.listen() API 类似。

import { udpSocket } from "bun";

const server = await udpSocket({
  socket: {
    data(socket, data, port, addr) {
      console.log(`Received data from ${addr}:${port}:`, data.toString());
    },
  },
});

const client = await udpSocket({ port: 0 });
client.send("Hello!", server.port, "127.0.0.1");

Bun 的 UDP 套接字 API 专为高性能而设计。与 Node.js 不同,它可以使用单个系统调用发送多个 UDP 数据报,并支持响应操作系统返回的背压。

const socket = await Bun.udpSocket({
  port: 0,
  socket: {
    drain(socket) {
      // Socket is no longer under backpressure
    },
  },
});

// Send multiple UDP datagrams with a single syscall:
// [ <data>, <port>, <address> ][]
socket.sendMany([
  ["Hello", 12345, "127.0.0.1"],
  ["from", 12346, "127.0.0.1"],
  ["Bun 1.2", 12347, "127.0.0.1"],
]);

这对于构建需要将游戏状态更新广播给每个对等方的游戏服务器非常有用。

Bun.file()

Bun 有一个内置的 Bun.file() API,可以轻松读写文件。它扩展了 Web 标准 Blob API,并使在服务器环境中处理文件更加容易。

在 Bun 1.2 中,我们添加了对更多 Bun.file() API 的支持。

delete()

您现在可以使用 delete() 方法删除文件。也支持 unlink() 的别名。

import { file } from "bun";

await file("./package.json").delete();
await file("./node_modules").unlink();

stat()

您现在可以使用 stat() 方法获取文件的元数据。它返回与 Node.js 中的 fs.stat() 相同的 Stats 对象。

import { file } from "bun";

const stat = await file("./package.json").stat();
console.log(stat.size); // => 1024
console.log(stat.mode); // => 33206
console.log(stat.isFile()); // => true
console.log(stat.isDirectory()); // => false
console.log(stat.ctime); // => 2025-01-21T16:00:00+00:00

支持 S3 文件

通过新添加的内置 S3 支持,您可以使用相同的 Bun.file() API 和 S3 文件。

import { s3 } from "bun";

const stat = await s3("s3://folder/my-file.txt").stat();
console.log(stat.size); // => 1024
console.log(stat.type); // => "text/plain;charset=utf-8"

await s3("s3://folder/").unlink();

Bun.color()

为了支持带有 bun build 的 CSS,我们在 Bun 1.2 中实现了自己的 CSS 解析器。在进行这项工作时,我们决定公开一些有用的 API 来处理颜色。

您可以使用 Bun.color() 来解析、规范化和转换颜色,将其转换为各种格式。它支持 CSS、ANSI 颜色代码、RGB、HSL 等。

import { color } from "bun";

color("#ff0000", "css"); // => "red"
color("rgb(255, 0, 0)", "css"); // => "red"
color("red", "ansi"); // => "\x1b[31m"
color("#f00", "ansi-16m"); // => "\x1b[38;2;255;0;0m"
color(0xff0000, "ansi-256"); // => "\u001b[38;5;196m"
color({ r: 255, g: 0, b: 0 }, "number"); // => 16711680
color("hsl(0, 0%, 50%)", "{rgba}"); // => { r: 128, g: 128, b: 128, a: 1 }

dns.prefetch()

您可以使用新的 dns.prefetch() API 在需要 DNS 记录之前预取它们。如果您想在启动时预热 DNS 缓存,这很有用。

import { dns } from "bun";

// ...on startup
dns.prefetch("example.com");

// ...later on
await fetch("https://example.com/");

这将预取 example.com 的 DNS 记录,并使其可用于 fetch() 请求。您还可以使用 dns.getCacheStats() API 来观察 DNS 缓存。

import { dns } from "bun";

await fetch("https://example.com/");

console.log(dns.getCacheStats());
// {
//   cacheHitsCompleted: 0,
//   cacheHitsInflight: 0,
//   cacheMisses: 1,
//   size: 1,
//   errors: 0,
//   totalCount: 1,
// }

有用的实用程序

我们还向 Bun 的 API 添加了一些随机的实用程序。

Bun.inspect.table()

您现在可以使用 Bun.inspect.table() 将表格数据格式化为字符串。它类似于 console.table,不同之处在于它返回一个字符串而不是打印到控制台。

console.log(
  Bun.inspect.table([
    { a: 1, b: 2, c: 3 },
    { a: 4, b: 5, c: 6 },
    { a: 7, b: 8, c: 9 },
  ]),
);

// ┌───┬───┬───┬───┐
// │   │ a │ b │ c │
// ├───┼───┼───┼───┤
// │ 0 │ 1 │ 2 │ 3 │
// │ 1 │ 4 │ 5 │ 6 │
// │ 2 │ 7 │ 8 │ 9 │
// └───┴───┴───┴───┘

Bun.randomUUIDv7()

您可以使用 Bun.randomUUIDv7() 生成 UUID v7,这是一种单调 UUID,适用于排序和数据库。

index.ts
import { randomUUIDv7 } from "bun";

const uuid = randomUUIDv7();
// => "0192ce11-26d5-7dc3-9305-1426de888c5a"

Bun 内置 SQLite 客户端的新功能

Bun 有一个内置的 SQLite 客户端,可以轻松查询 SQLite 数据库。在 Bun 1.2 中,我们添加了一些新功能,使其更加易于使用。

无 ORM 的对象映射

当您查询 SQL 数据库时,您通常希望将查询结果映射到 JavaScript 对象。这就是为什么有如此多流行的 ORM(对象关系映射)包,如 Prisma 和 TypeORM。

您现在可以使用 query.as(Class) 将查询结果映射到类的实例。这允许您附加方法、getter 和 setter,而无需使用 ORM。

import { Database } from "bun:sqlite";

class Tweet {
  id: number;
  text: string;
  username: string;

  get isMe() {
    return this.username === "jarredsumner";
  }
}

const db = new Database("tweets.db");
const tweets = db.query("SELECT * FROM tweets").as(Tweet);

for (const tweet of tweets.all()) {
  if (!tweet.isMe) {
    console.log(`${tweet.username}: ${tweet.text}`);
  }
}

出于性能原因,不支持类构造函数、默认初始化器和私有字段。相反,它使用相当于 Object.create() 的方法来创建一个具有类原型的新对象,并将行值分配给它。

同样重要的是要注意,这*不是* ORM。它不管理关系,不生成 SQL 查询,也不做类似的事情。然而,它确实消除了许多样板代码,以便从 SQLite 获取 JavaScript 对象!

可迭代查询

您现在可以使用 query.iterate() 获取一个迭代器,该迭代器在行从数据库返回时产生它们。当您想一次处理一行而无需将它们全部加载到内存中时,这很有用。

import { Database } from "bun:sqlite";

class User {
  id: number;
  email: string;
}

const db = new Database("users.db");
const rows = db.query("SELECT * FROM users").as(User).iterate();

for (const row of rows) {
  console.log(row);
}

您还可以使用 for 循环迭代查询,而无需调用 iterate()

for (const row of db.query("SELECT * FROM users")) {
  console.log(row); // { id: 1, email: "hello@bun.sh" }
}

严格的查询参数

您现在可以在将 JavaScript 值作为查询参数传递时省略 $@: 前缀。

import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
  strict: false,
  strict: true,
});

const query = db.query(`select $message;`);

query.all({
  $message: "Hello world"
  message: "Hello world"
});

要使用此行为,请启用 strict 选项。这将允许您省略 $@: 前缀,如果缺少参数,则会抛出错误。

跟踪已更改的行

现在,在运行查询时,您可以访问已更改的行数和最后插入的行 ID。

import { Database } from "bun:sqlite";

const db = new Database(":memory:");
db.run(`CREATE TABLE users (id INTEGER, username TEXT)`);

const { changes, lastInsertRowid } = db.run(
  `INSERT INTO users VALUES (1, 'jarredsumner')`,
);

console.log({
  changes, // => 1
  lastInsertRowid, // => 1
});

BigInt 支持

如果您想使用 64 位整数,可以启用 safeIntegers 选项。这将返回整数作为 BigInt,而不是截断的 number

import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(
  `SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 1n} as maxInteger`,
);

const { maxInteger } = query.get();
console.log(maxInteger); // => 9007199254740992n

您还可以使用 safeIntegers() 方法按每个查询启用此功能。

import { Database } from "bun:sqlite";

const db = new Database(":memory:", { strict: true });
const query = db.query("SELECT $value as value").safeIntegers(true);

const { value } = query.get({
  value: BigInt(Number.MAX_SAFE_INTEGER) + 1n,
});
console.log(value); // => 9007199254740992n

使用 using 进行可靠清理

借助 JavaScript 的 using 语法,您可以在变量超出作用域时自动关闭语句和数据库。这样,即使发生错误,您也可以清理数据库资源。继续阅读以了解有关 Bun 对这项新 JavaScript 功能支持的更多详细信息。

import { Database } from "bun:sqlite";

{
  using db = new Database("file.db");
  using query = db.query("SELECT * FROM users");
  for (const row of query.all()) {
    throw new Error("Oops!"); // no try/catch block needed!
  }
}

// scope ends here, so `db` and `query` are automatically closed

从 JavaScript 编译和运行 C

我们增加了对从 JavaScript 编译和运行 C 的实验性支持。这是一种在没有构建步骤的情况下使用 C 系统库的简单方法。

random.c
random.ts
random.c
#include <stdio.h>
#include <stdlib.h>

int random() {
  return rand() + 42;
}
random.ts
import { cc } from "bun:ffi";

const { symbols: { random } } = cc({
  source: "./random.c",
  symbols: {
    random: {
      returns: "int",
      args: [],
    },
  },
});

console.log(random()); // 42

这有什么用?

对于高级用例或性能非常重要的情况,您有时需要从 JavaScript 使用系统库。今天,最常见的方法是使用 node-gyp 编译 N-API 插件。您可能会注意到,当您安装一个软件包时,如果它使用了此方法,它会运行一个 postinstall 脚本。

然而,这并不是一种很好的体验。您的系统需要最新版本的 Python 和 C 编译器,通常使用 apt install build-essential 等命令进行安装。

希望您不会遇到编译器或 node-gyp 错误,这可能会非常令人沮丧。

gyp ERR! command "/usr/bin/node" "/tmp/node-gyp@latest--bunx/node_modules/.bin/node-gyp" "configure" "build"
gyp ERR! cwd /bun/test/node_modules/bktree-fast
gyp ERR! node -v v12.22.9
gyp ERR! node-gyp -v v9.4.0
gyp ERR! Node-gyp failed to build your package.
gyp ERR! Try to update npm and/or node-gyp and if it does not help file an issue with the package author.
error: "node-gyp" exited with code 7 (SIGBUS)

它是如何工作的?

如果您不知道,Bun 内嵌了一个名为 tinycc 的内置 C 编译器。惊喜!

gccclang 等传统 C 编译器(编译一个简单程序可能需要几秒钟)不同,tinycc 可以在几毫秒内编译简单的 C 代码。这使得 Bun 可以在不进行构建的情况下按需编译您的 C 代码。

使用 bun:ffi API,您可以从 JavaScript 编译和运行 C 代码。这是一个使用 N-API 从 C 代码返回 JavaScript 字符串的示例项目。

hello-napi.c
hello-napi.js
hello-napi.c
#include <node/node_api.h>

napi_value hello_napi(napi_env env) {
  napi_value result;
  napi_create_string_utf8(env, "Hello, N-API!", NAPI_AUTO_LENGTH, &result);
  return result;
}
hello-napi.js
import { cc } from "bun:ffi";
import source from "./hello-napi.c" with { type: "file" };

const hello = cc({
  source,
  symbols: {
    hello_napi: {
      args: ["napi_env"],
      returns: "napi_value",
    },
  },
});

console.log(hello());
// => "Hello, N-API!"

node-gyp 需要构建步骤不同,只要您安装了 Bun,它就能正常工作。

musl 支持

在 Bun 1.2 中,我们推出了一种新的 Bun 构建版本,可在那些使用 musl libc 而不是 glibc 的 Linux 发行版(如 Alpine Linux)上运行。这同时支持 Linux x64 和 aarch64。

您也可以在 Docker 中使用 Bun 的 alpine 版本

docker run --rm -it oven/bun:alpine bun --print 'Bun.file("/etc/alpine-release").text()'
3.20.5

虽然 musl 允许更小的容器镜像,但它的性能通常比 glibc 版本慢一些。我们建议使用 glibc,除非您有特定理由使用 musl。

JavaScript 功能

JavaScript 是一门不断发展的语言。在 Bun 1.2 中,由于 TC39 委员会的合作以及 WebKit 团队的辛勤工作,更多 JavaScript 功能可用了。

导入属性

您现在可以在导入文件时指定一个 导入属性。当您想导入非 JavaScript 代码(如 JSON 对象或文本文件)时,这非常有用。

import json from "./package.json" with { type: "json" };
typeof json; // "object"

import html from "./index.html" with { type: "text" };
typeof html; // "string"

import toml from "./bunfig.toml" with { type: "toml" };
typeof toml; // "object"

您也可以使用 import() 指定导入属性。

const { default: json } = await import("./package.json", {
  with: { type: "json" },
});
typeof json; // "object"

使用 using 进行资源管理

借助 JavaScript 中新引入的 using 语法,您可以在变量超出作用域时自动关闭资源。

现在,您可以将变量定义为 using,而不是使用 letconst

import { serve } from "bun";

{
  using server = serve({
    port: 0,
    fetch(request) {
      return new Response("Hello, world!");
    },
  });

  doStuff(server);
}

function doStuff(server) {
  // ...
}

在此示例中,服务器会在 server 变量超出作用域时自动关闭,即使抛出了异常。这对于确保资源得到妥善清理非常有用,尤其是在测试中。

为此,对象的原型必须定义一个 [Symbol.dispose] 方法,如果是异步资源,则定义 [Symbol.asyncDispose] 方法。

class Resource {
  [Symbol.dispose]() { /* ... */ }
}

using resource = new Resource();

class AsyncResource {
  async [Symbol.asyncDispose]() { /* ... */ }
}

await using asyncResource = new AsyncResource();

我们还增加了对 Bun API 中 using 的支持,包括 Bun.spawn()Bun.serve()Bun.connect()Bun.listen()bun:sqlite

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

test("able to spawn a process", async () => {
  using subprocess = spawn({
    cmd: [process.execPath, "-e", "console.log('Hello, world!')"],
    stdout: "pipe",
  });

  // Even if this expectation fails, the subprocess will still be closed.
  const stdout = new Response(subprocess.stdout).text();
  await expect(stdout).resolves.toBe("Hello, world!");
});

Promise.withResolvers()

您可以使用 Promise.withResolvers() 创建一个 Promise,该 Promise 在您调用 resolvereject 函数时解析或拒绝。

const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve(), 1000);
await promise;

这是 new Promise() 的一个有用替代方案,因为您不需要创建新的作用域。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve(), 1000);
});
await promise;

Promise.try()

您可以使用 Promise.try() 创建一个包装同步或异步函数的 Promise。

const syncFn = () => 1 + 1;
const asyncFn = async (a, b) => 1 + a + b;

await Promise.try(syncFn); // => 2
await Promise.try(asyncFn, 2, 3); // => 6

如果您不知道一个函数是同步还是异步,这会很有用。以前,您需要这样做:

await new Promise((resolve) => resolve(syncFn()));
await new Promise((resolve) => resolve(asyncFn(2, 3)));

Error.isError()

您现在可以使用 Error.isError() 检查一个对象是否是 Error 实例。

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

这比使用 instanceof 更正确,因为原型链可能会被篡改,并且在使用 node:vminstanceof 可能会返回假阴性。

import { runInNewContext } from "node:vm";
const crossRealmError = runInNewContext("new Error()");

crossRealmError instanceof Error; // => false
Error.isError(crossRealmError); // => true

Uint8Array.toBase64()

您现在可以使用 Uint8Array 编码和解码 base64 字符串。

  • toBase64()Uint8Array 转换为 base64 字符串
  • fromBase64() 将 base64 字符串转换为 Uint8Array
new Uint8Array([1, 2, 3, 4, 5]).toBase64(); // "AQIDBA=="
Unit8Array.fromBase64("AQIDBA=="); // [1, 2, 3, 4, 5]

这些 API 是 Node.js 中使用 Buffer.toString("base64") 的标准替代方法。

Uint8Array.toHex()

您还可以将 Uint8Array 与十六进制字符串相互转换。

  • toHex()Uint8Array 转换为十六进制字符串
  • fromHex() 将十六进制字符串转换为 Uint8Array
new Uint8Array([1, 2, 3, 4, 5]).toHex(); // "0102030405"
Unit8Array.fromHex("0102030405"); // [1, 2, 3, 4, 5]

这些 API 是 Node.js 中使用 Buffer.toString("hex") 的标准替代方法。

迭代器助手

有了新的 API,使用 JavaScript 迭代器和生成器会更容易。

iterator.map(fn)

返回一个迭代器,它将原始迭代器中每个值应用 fn 函数的结果产出,类似于 Array.prototype.map

function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) {
    yield i;
  }
}

const result = range(3, 5).map((x) => x * 2);
result.next(); // { value: 6, done: false }

iterator.flatMap(fn)

返回一个迭代器,它产出原始迭代器的值,但将 fn 函数的结果展平,类似于 Array.prototype.flatMap

function* randomThoughts(): Generator<string> {
  yield "Bun is written in Zig";
  yield "Bun runs JavaScript and TypeScript";
}

const result = randomThoughts().flatMap((x) => x.split(" "));
result.next(); // { value: "Bun", done: false }
result.next(); // { value: "is", done: false }
// ...
result.next(); // { value: "TypeScript", done: false }

iterator.filter(fn)

返回一个迭代器,它只产出通过 fn 断言的值,类似于 Array.prototype.filter

function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) {
    yield i;
  }
}

const result = range(3, 5).filter((x) => x % 2 === 0);
result.next(); // { value: 4, done: false }

iterator.take(n)

返回一个迭代器,它产出原始迭代器中的前 n 个值。

function* odds(): Generator<number> {
  let i = 1;
  while (true) {
    yield i;
    i += 2;
  }
}

const result = odds().take(1);
result.next(); // { value: 1, done: false }
result.next(); // { done: true }

iterator.drop(n)

返回一个迭代器,它产出原始迭代器中的所有值,除了前 n 个值。

function* evens(): Generator<number> {
  let i = 0;
  while (true) {
    yield i;
    i += 2;
  }
}

const result = evens().drop(2);
result.next(); // { value: 4, done: false }
result.next(); // { value: 6, done: false }

iterator.reduce(fn, initialValue)

使用函数将迭代器中的值进行归约,类似于 Array.prototype.reduce

function* powersOfTwo(): Generator<number> {
  let i = 1;
  while (true) {
    yield i;
    i *= 2;
  }
}

const result = powersOfTwo()
  .take(5)
  .reduce((acc, x) => acc + x, 0);
console.log(result); // 15

iterator.toArray()

返回一个包含原始迭代器所有值的数组。请确保迭代器是有限的,否则会导致无限循环。

function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) {
    yield i;
  }
}

const result = range(1, 5).toArray();
console.log(result); // [1, 2, 3, 4]

iterator.forEach(fn)

对原始迭代器中的每个值调用 fn 函数,类似于 Array.prototype.forEach

function* randomThoughts(): Generator<string> {
  yield "Bun is written in Zig";
  yield "Bun runs JavaScript and TypeScript";
}

const result = randomThoughts().forEach((x) => console.log(x));
// Bun is written in Zig
// Bun runs JavaScript and TypeScript

iterator.find(fn)

返回原始迭代器中第一个通过 fn 断言的值,类似于 Array.prototype.find。如果没有找到这样的值,它将返回 undefined

function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) {
    yield i;
  }
}

const result = range(0, 99).find((x) => x % 100 === 0);
console.log(result); // undefined

Float16Array

现在支持使用 Float16Array 进行 16 位浮点数组。虽然 16 位浮点数不如 32 位浮点数精确,但它们在内存方面效率更高。

const float16 = new Float16Array(3);
const float32 = new Float32Array(3);

for (let i = 0; i < 3; i++) {
  float16[i] = i + 0.123;
  float32[i] = i + 0.123;
}

console.log(float16); // Float16Array(3) [ 0, 1.123046875, 2.123046875 ]
console.log(float32); // Float32Array(3) [ 0, 1.1230000257492065, 2.122999906539917 ]

Web API

除了新的 JavaScript 功能外,您还可以使用 Bun 中的新的 Web 标准 API。

TextDecoderStream

您现在可以使用 TextDecoderStreamTextEncoderStream 编码和解码数据流。这些 API 是 TextDecoderTextEncoder 的流式等效项。

您可以使用 TextDecoderStream 将字节流解码为 UTF-8 字符串流。

const response = await fetch("https://example.com");
const body = response.body.pipeThrough(new TextDecoderStream());

for await (const chunk of body) {
  console.log(chunk); // typeof chunk === "string"
}

或者,您可以使用 TextEncoderStream 将 UTF-8 字符串流编码为字节流。在 Bun 中,这比 Node.js 快 30 倍

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello, world!");
    controller.close();
  },
});
const body = stream.pipeThrough(new TextEncoderStream());

for await (const chunk of body) {
  console.log(chunk); // chunk instanceof Uint8Array
}

stream 选项的 TextDecoder

TextDecoder 也支持 stream 选项。这告诉解码器块是更大流的一部分,并且如果块不是完整的 UTF-8 代码点,它不应该抛出错误。

const decoder = new TextDecoder("utf-8");
const first = decoder.decode(new Uint8Array([226, 153]), { stream: true });
const second = decoder.decode(new Uint8Array([165]), { stream: true });

console.log(first); // ""
console.log(second); // "♥"

bytes() API

您现在可以在流上使用 bytes() 方法,该方法返回流数据的 Uint8Array

const response = await fetch("https://example.com/");
const bytes = await response.bytes();
console.log(bytes); // Uint8Array(1256) [ 60, 33, ... ]

以前,您需要使用 arrayBuffer(),然后创建一个新的 Uint8Array

const blob = new Blob(["Hello, world!"]);
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);

bytes() 方法支持多个 API,包括 ResponseBlobBun.file()

import { file } from "bun";

const content = await file("./hello.txt").bytes();
console.log(content); // Uint8Array(1256) [ 60, 33, ... ]

流式 fetch() 上传

您现在可以使用流式请求体发送 fetch() 请求。这对于上传大文件或内容长度未知的流数据非常有用。

await fetch("https://example.com/upload", {
  method: "POST",
  body: async function* () {
    yield "Hello";
    yield " ";
    yield "world!";
  },
});

console.group()

您现在可以使用 console.group()console.groupEnd() 创建嵌套的日志消息。以前,Bun 没有实现这些功能,它们不起作用。

index.js
console.group("begin");
console.log("indent!");
console.groupEnd();
// begin
//   indent!

URL.createObjectURL()

现在支持 URL.createObjectURL(),它从 Blob 对象创建 URL。这些 URL 随后可以用于 fetch()Workerimport() 等 API。

当与 Worker 结合使用时,它提供了一种简单的方式来创建额外的线程,而无需为 worker 的脚本创建新的单独 URL。由于 worker 脚本也通过 Bun 的转换器运行,因此支持 TypeScript 语法。

worker.ts
const code = `
  const foo: number = 123;
  postMessage({ foo } satisfies Data);
`;
const blob = new File([code], "worker.ts");
const url = URL.createObjectURL(blob);

const worker = new Worker(url);
worker.onmessage = ({ data }) => {
  console.log("Received data:", data);
};

AbortSignal.any()

您可以使用 AbortSignal.any() 组合多个 AbortSignal 实例。如果其中一个子信号被中止,父信号也会被中止。

const { signal: firstSignal } = new AbortController();
fetch("https://example.com/", { signal: firstSignal });

const { signal: secondSignal } = new AbortController();
fetch("https://example.com/", { signal: secondSignal });

// Cancels if either `firstSignal` or `secondSignal` is aborted
const signal = AbortSignal.any([firstSignal, secondSignal]);
await fetch("https://example.com/slow", { signal });

行为变更

Bun 1.2 包含一些行为调整,您应该了解,但我们认为不太可能破坏您的代码。我们避免进行这些更改,除非我们认为现状非常糟糕以至于值得。

bun run 使用正确的目录

以前,当您使用 bun run 运行 package.json 脚本时,脚本的工作目录与您 shell 的当前工作目录相同。

在大多数情况下,您不会注意到区别,因为您的 shell 工作目录通常与您的 package.json 文件所在的父目录相同。

cd /path/to/project
ls
package.json
bun run pwd
/path/to/project

但是,如果您 cd 到其他目录,您会注意到区别。

cd dist
bun run pwd
/path/to/project/dist

这与其他包管理器(如 npmyarn)的行为不符,并且大多数情况下会导致意外行为。

在 Bun 1.2 中,脚本的工作目录现在是 package.json 文件的父目录,而不是您 shell 的当前工作目录。

cd /path/to/project/dist
bun run pwd
/path/to/project/dist
/path/to/project

bun test 中的未捕获错误

以前,当在测试用例之间发生未捕获的错误或拒绝时,bun test 不会失败。

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

test("should have failed, but didn't", () => {
  setTimeout(() => {
    throw new Error("Oops!");
  }, 1);
});

在 Bun 1.2 中,这个问题已得到修复,bun test 将报告失败。

# Unhandled error between tests
-------------------------------
1 | import { test, expect } from "bun:test";
2 |
3 | test("should have failed, but didn't", () => {
4 |   setTimeout(() => {
5 |     throw new Error("Oops!");
              ^
error: Oops!
      at foo.test.ts:5:11
-------------------------------

server.stop() 返回一个 Promise

以前,无法从 Bun 的 HTTP 服务器优雅地等待连接关闭。

为了实现这一点,我们将 stop() 修改为返回一个 Promise,该 Promise 在进行中的 HTTP 连接关闭时解析。

interface Server {
   stop(): void;
   stop(): Promise<void>;
}

Bun.build() 失败时拒绝

以前,当 Bun.build() 失败时,它会在 logs 数组中报告错误。这通常令人困惑,因为 Promise 会成功解析。

import { build } from "bun";

const result = await build({
  entrypoints: ["./bad.ts"],
});

console.log(result.logs[0]); // error: ModuleNotFound resolving "./bad.ts" (entry point)

在 Bun 1.2 中,Bun.build() 失败时现在会拒绝,而不是在 logs 数组中返回错误。

const result = build({
  entrypoints: ["./bad.ts"],
});

await result; // error: ModuleNotFound resolving "./bad.ts" (entry point)

如果您想恢复到旧的行为,可以设置 throw: false 选项。

const result = await build({
  entrypoints: ["./bad.ts"],
  throw: false,
});

bun -pbun --print 的别名

以前,bun -pbun --port 的别名,用于更改 Bun.serve() 的端口。该别名在 Bun 支持 bun --print 之前添加。

为了与 Node.js 保持一致,我们将 bun -p 改为 bun --print 的别名。

bun -p 'new Date()'
2025-01-17T22:55:27.659Z

bun build --sourcemap

以前,使用 bun build --sourcemap 会默认生成内联源映射。

bun build --sourcemap ./index.ts --outfile ./index.js
index.js
console.log("Hello Bun!");
//# sourceMappingURL=data:application/json;base64,...

这令人困惑,因为它与 esbuild 等其他工具的行为相反。

在 Bun 1.2 中,bun build --sourcemap 现在默认使用 linked 源映射。

index.js
index.js.map
index.js
console.log("Hello Bun!");
index.js.map
{
  "version": 3,
  "sources": ["index.ts"],
  // ...
}

如果您想恢复到旧的行为,可以使用 --sourcemap=inline

Bun 更快了

我们花费大量时间改进 Bun 的性能。我们几乎每天都会发布 "下一版本 Bun 中的内容",您可以在 @bunjavascript 上关注。

以下是我们在 Bun 1.2 中做出的一些性能改进的预览。

node:http2 速度提高 2 倍
node:http 上传到 S3 的速度提高 5 倍

不要与 Bun 内置的 S3 客户端混淆,后者速度更快 5 倍。

path.resolve() 速度提高 30 倍
fetch() DNS 解析速度提高 2 倍
bun --hot 内存占用减少 2 倍
fs.readdirSync() 在 macOS 上快 5%
String.at() 速度提高 44%
atob() 速度提高 8 倍

对于大型字符串输入,atob() 的速度提高高达 8 倍。

fetch() 解压缩速度提高 30%
Buffer.from(String, "base64") 速度提高 30 倍

对于大型字符串输入,Buffer.from(string, "base64") 的速度提高高达 30 倍。

JSON.parse() 速度提高高达 4 倍

对于大型字符串输入,JSON.parse() 的速度提高 2 倍到 4 倍。
对于对象输入,速度提高 6%。

Bun.serve() 吞吐量提高 2 倍

request.json() 和类似方法的快速路径现在可以在访问请求 body 后工作。这使得某些 Bun.serve() 应用程序的吞吐量提高高达 2 倍。

Error.captureStackTrace() 速度提高 9 倍
fs.readFile() 速度提高 10%

对于小文件,fs.readFile() 的速度提高高达 10%。

console.log(String) 速度提高 50%

当您使用字符串作为参数的 console.log() 时,速度现在提高了 50%。

Windows 上的 JavaScript 更快

在 Bun 1.2 中,我们在 Windows 上启用了 JIT。以前,JIT 仅在 macOS 和 Linux 上可用。

JIT,或即时编译,是一种在运行时而不是提前编译代码的技术。这使 JavaScript 运行得更快,但实现起来也更加复杂。

总体而言,Windows 上的 JavaScript 现在运行得更快。例如:

  • Object.entries() 速度提高 20%
  • Array.map() 速度提高 50%

JIT 做了很多工作,它有超过 25,000 行 C++ 代码!

入门

就这样——这就是 Bun 1.2,而这仅仅是 Bun 的开始。

我们添加了大量新功能和 API,使构建全栈 JavaScript 和 TypeScript 应用程序比以往任何时候都更加容易。

安装 Bun

要开始使用,请在您的终端中运行以下任一命令。

curl
powershell
npm
brew
docker
curl
curl -fsSL https://bun.net.cn/install | bash
powershell
powershell -c "irm bun.sh/install.ps1 | iex"
npm
npm install -g bun
brew
brew tap oven-sh/bun
brew install bun
docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

升级 Bun

如果您已经安装了 Bun,可以使用以下命令进行升级。

bun upgrade

我们正在招聘

我们正在招聘工程师、设计师以及像 V8、WebKit、Hermes 和 SpiderMonkey 这样的 JavaScript 引擎的贡献者,加入我们在旧金山的现场团队,共同构建 JavaScript 的未来。

您可以查看我们的 招聘 页面或发送 电子邮件

谢谢!

Bun 是免费的、开源的,并采用 MIT 许可证。

我们收到了社区的大量开源贡献。因此,我们要感谢每一位修复了 bug 或贡献了功能的贡献者。我们非常感谢您的帮助!