Bun

单文件可执行文件

Bun 的 bundler 实现了一个 --compile 标志,用于从 TypeScript 或 JavaScript 文件生成独立的二进制文件。

bash
cli.ts
bun build ./cli.ts --compile --outfile mycli
cli.ts
console.log("Hello world!");

这会将 cli.ts 捆绑成一个可直接执行的二进制文件

$ ./mycli
Hello world!

所有导入的文件和包,以及 Bun 运行时的副本,都会被捆绑到可执行文件中。所有内置的 Bun 和 Node.js API 都受支持。

交叉编译到其他平台

--target 标志允许您为不同于运行 bun build 的机器的操作系统、架构或 Bun 版本编译您的独立可执行文件。

为 Linux x64(大多数服务器)构建

bun build --compile --target=bun-linux-x64 ./index.ts --outfile myapp

# To support CPUs from before 2013, use the baseline version (nehalem)
bun build --compile --target=bun-linux-x64-baseline ./index.ts --outfile myapp

# To explicitly only support CPUs from 2013 and later, use the modern version (haswell)
# modern is faster, but baseline is more compatible.
bun build --compile --target=bun-linux-x64-modern ./index.ts --outfile myapp

为 Linux ARM64(例如 Graviton 或 Raspberry Pi)构建

# Note: the default architecture is x64 if no architecture is specified.
bun build --compile --target=bun-linux-arm64 ./index.ts --outfile myapp

为 Windows x64 构建

bun build --compile --target=bun-windows-x64 ./path/to/my/app.ts --outfile myapp

# To support CPUs from before 2013, use the baseline version (nehalem)
bun build --compile --target=bun-windows-x64-baseline ./path/to/my/app.ts --outfile myapp

# To explicitly only support CPUs from 2013 and later, use the modern version (haswell)
bun build --compile --target=bun-windows-x64-modern ./path/to/my/app.ts --outfile myapp

# note: if no .exe extension is provided, Bun will automatically add it for Windows executables

为 macOS arm64 构建

bun build --compile --target=bun-darwin-arm64 ./path/to/my/app.ts --outfile myapp

为 macOS x64 构建

bun build --compile --target=bun-darwin-x64 ./path/to/my/app.ts --outfile myapp

支持的目标

--target 标志的顺序无关紧要,只要它们由 - 分隔即可。

--target操作系统架构现代基础Libc
bun-linux-x64Linuxx64glibc
bun-linux-arm64Linuxarm64N/Aglibc
bun-windows-x64Windowsx64-
bun-windows-arm64Windowsarm64-
bun-darwin-x64macOSx64-
bun-darwin-arm64macOSarm64N/A-
bun-linux-x64-muslLinuxx64musl
bun-linux-arm64-muslLinuxarm64N/Amusl

在 x64 平台上,Bun 使用 SIMD 优化,这需要支持 AVX2 指令的现代 CPU。Bun 的 -baseline 版本适用于不支持这些优化的旧 CPU。通常,当您安装 Bun 时,我们会自动检测使用哪个版本,但在交叉编译时可能难以做到,因为您可能不知道目标 CPU。在 Darwin x64 上您通常不需要担心这个问题,但在 Windows x64 和 Linux x64 上则需要考虑。如果您或您的用户看到 "Illegal instruction" 错误,您可能需要使用基础版本。

构建时常量

使用 --define 标志可将构建时常量注入可执行文件中,例如版本号、构建时间戳或配置值

bun build --compile --define BUILD_VERSION='"1.2.3"' --define BUILD_TIME='"2024-01-15T10:30:00Z"' src/cli.ts --outfile mycli

这些常量在构建时直接嵌入到编译后的二进制文件中,提供零运行时开销,并支持死代码消除优化。

有关全面的示例和高级模式,请参阅 构建时常量指南

部署到生产环境

编译后的可执行文件可减少内存使用量并提高 Bun 的启动时间。

通常,Bun 在 importrequire 时会读取并转译 JavaScript 和 TypeScript 文件。这也是 Bun 的许多功能“开箱即用”的原因之一,但这并非没有成本。读取磁盘上的文件、解析文件路径、解析、转译和打印源代码都需要时间和内存。

通过编译后的可执行文件,您可以将这些成本从运行时转移到构建时。

部署到生产环境时,我们建议执行以下操作

bun build --compile --minify --sourcemap ./path/to/my/app.ts --outfile myapp

字节码编译

为了提高启动时间,请启用字节码编译

bun build --compile --minify --sourcemap --bytecode ./path/to/my/app.ts --outfile myapp

通过字节码编译,tsc 启动速度提高 2 倍

字节码编译将大型输入文件的解析开销从运行时移至捆绑时。您的应用程序启动速度会更快,但 bun build 命令会稍慢一些。它不会混淆源代码。

实验性功能: 字节码编译是 Bun v1.1.30 中引入的一项实验性功能。目前仅支持 cjs 格式(这意味着不支持顶层 await)。如果您遇到任何问题,请告知我们!

这些标志有什么作用?

--minify 参数会优化转译后输出代码的大小。如果您有一个大型应用程序,这可以节省兆字节的空间。对于较小的应用程序,它可能仍然能稍微提高启动时间。

--sourcemap 参数会嵌入一个使用 zstd 压缩的 sourcemap,这样错误和堆栈跟踪会指向原始位置而不是转译后的位置。Bun 在发生错误时会自动解压缩和解析 sourcemap。

--bytecode 参数启用字节码编译。每次您在 Bun 中运行 JavaScript 代码时,JavaScriptCore(引擎)都会将您的源代码编译成字节码。我们可以将此解析工作从运行时转移到捆绑时,从而节省您的启动时间。

嵌入运行时参数

--compile-exec-argv="args" - 嵌入可通过 process.execArgv 访问的运行时参数

bun build --compile --compile-exec-argv="--smol --user-agent=MyBot" ./app.ts --outfile myapp
// In the compiled app
console.log(process.execArgv); // ["--smol", "--user-agent=MyBot"]

充当 Bun CLI

Bun v1.2.16 新增功能

您可以将独立的可执行文件作为 bun CLI 本身来运行,只需设置 BUN_BE_BUN=1 环境变量。当设置此变量时,可执行文件将忽略其捆绑的入口点,而是公开 Bun CLI 的所有功能。

例如,考虑一个由简单脚本编译而成的可执行文件

cat such-bun.js
console.log("you shouldn't see this");
bun build --compile ./such-bun.js
 [3ms] bundle 1 modules
[89ms] compile such-bun

通常,运行带参数的 ./such-bun 会执行脚本。但是,当设置 BUN_BE_BUN=1 环境变量时,它的行为就像 bun 二进制文件一样

# Executable runs its own entrypoint by default
./such-bun install
you shouldn't see this

# With the env var, the executable acts like the `bun` CLI
BUN_BE_BUN=1 ./such-bun install
bun install v1.2.16-canary.1 (1d1db811)
Checked 63 installs across 64 packages (no changes) [5.00ms]

这对于构建基于 Bun 的 CLI 工具非常有用,这些工具可能需要安装包、捆绑依赖项、运行不同的或本地文件等等,而无需下载单独的二进制文件或安装 bun。

全栈可执行文件

Bun v1.2.17 新增功能

Bun 的 --compile 标志可以创建包含服务器端和客户端代码的独立可执行文件,这使其非常适合全栈应用程序。当您在服务器代码中导入 HTML 文件时,Bun 会自动捆绑所有前端资源(JavaScript、CSS 等)并将其嵌入到可执行文件中。当 Bun 在服务器端看到 HTML 导入时,它会启动前端构建过程来捆绑 JavaScript、CSS 和其他资源。

server.ts
index.html
app.js
styles.css
server.ts
import { serve } from "bun";
import index from "./index.html";

const server = serve({
  routes: {
    "/": index,
    "/api/hello": { GET: () => Response.json({ message: "Hello from API" }) },
  },
});

console.log(`Server running at https://:${server.port}`);
index.html
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
    <link rel="stylesheet" href="./styles.css">
  </head>
  <body>
    <h1>Hello World</h1>
    <script src="./app.js"></script>
  </body>
</html>
app.js
console.log("Hello from the client!");
styles.css
body {
  background-color: #f0f0f0;
}

将此构建为单个可执行文件

bun build --compile ./server.ts --outfile myapp

这将创建一个包含以下内容的自包含二进制文件

  • 您的服务器代码
  • Bun 运行时
  • 所有前端资源(HTML、CSS、JavaScript)
  • 您的服务器使用的任何 npm 包

结果是一个单一文件,可以部署到任何地方,而无需安装 Node.js、Bun 或任何依赖项。只需运行

./myapp

Bun 会自动处理前端资源的提供,包括正确的 MIME 类型和缓存标头。HTML 导入被替换为一个清单对象,Bun.serve 使用该对象来高效地提供预捆绑的资源。

有关使用 Bun 构建全栈应用程序的更多详细信息,请参阅 全栈指南

Worker

要在独立的可执行文件中使用 worker,请将 worker 的入口点添加到 CLI 参数中

bun build --compile ./index.ts ./my-worker.ts --outfile myapp

然后,在您的代码中引用 worker

console.log("Hello from Bun!");

// Any of these will work:
new Worker("./my-worker.ts");
new Worker(new URL("./my-worker.ts", import.meta.url));
new Worker(new URL("./my-worker.ts", import.meta.url).href);

截至 Bun v1.1.25,当您向独立可执行文件添加多个入口点时,它们将被单独捆绑到可执行文件中。

将来,我们可能会自动检测 new Worker(path) 中对静态已知路径的使用,然后将它们捆绑到可执行文件中,但目前,您需要像上面的示例一样手动将其添加到 shell 命令中。

如果您使用指向未包含在独立可执行文件中的文件的相对路径,它将尝试从相对于进程当前工作目录的路径加载该文件(然后如果不存在则会报错)。

SQLite

您可以将 bun:sqlite 导入与 bun build --compile 一起使用。

默认情况下,数据库相对于进程的当前工作目录进行解析。

import db from "./my.db" with { type: "sqlite" };

console.log(db.query("select * from users LIMIT 1").get());

这意味着如果可执行文件位于 /usr/bin/hello,而用户的终端位于 /home/me/Desktop,它将查找 /home/me/Desktop/my.db

$ cd /home/me/Desktop
$ ./hello

嵌入资源和文件

独立可执行文件支持嵌入文件。

要使用 bun build --compile 将文件嵌入到可执行文件中,请在代码中导入该文件。

// this becomes an internal file path
import icon from "./icon.png" with { type: "file" };
import { file } from "bun";

export default {
  fetch(req) {
    // Embedded files can be streamed from Response objects
    return new Response(file(icon));
  },
};

可以使用 Bun.file 的函数或 Node.js 的 fs.readFile 函数(在 "node:fs" 中)来读取嵌入的文件。

例如,要读取嵌入文件的内容

import icon from "./icon.png" with { type: "file" };
import { file } from "bun";

const bytes = await file(icon).arrayBuffer();
// await fs.promises.readFile(icon)
// fs.readFileSync(icon)

嵌入 SQLite 数据库

如果您的应用程序需要嵌入 SQLite 数据库,请在 import 属性中设置 type: "sqlite",并将 embed 属性设置为 "true"

import myEmbeddedDb from "./my.db" with { type: "sqlite", embed: "true" };

console.log(myEmbeddedDb.query("select * from users LIMIT 1").get());

此数据库是读写的,但所有更改在可执行文件退出时都会丢失(因为它存储在内存中)。

嵌入 N-API 插件

从 Bun v1.0.23 开始,您可以将 .node 文件嵌入到可执行文件中。

const addon = require("./addon.node");

console.log(addon.hello());

不幸的是,如果您使用 @mapbox/node-pre-gyp 或其他类似工具,您需要确保 .node 文件是直接 require 的,否则将无法正确打包。

嵌入目录

要使用 bun build --compile 嵌入目录,请在 bun build 命令中使用 shell glob

bun build --compile ./index.ts ./public/**/*.png

然后,您可以在代码中引用这些文件

import icon from "./public/assets/icon.png" with { type: "file" };
import { file } from "bun";

export default {
  fetch(req) {
    // Embedded files can be streamed from Response objects
    return new Response(file(icon));
  },
};

这实际上是一个变通方法,我们期望在未来通过更直接的 API 来改进这一点。

列出嵌入的文件

要获取所有嵌入文件的列表,请使用 Bun.embeddedFiles

import "./icon.png" with { type: "file" };
import { embeddedFiles } from "bun";

console.log(embeddedFiles[0].name); // `icon-${hash}.png`

Bun.embeddedFiles 返回一个 Blob 对象数组,您可以使用它们来获取文件的大小、内容和其他属性。

embeddedFiles: Blob[]

嵌入文件的列表不包括已打包的源代码,如 .ts.js 文件。

内容哈希

默认情况下,嵌入文件在其名称后面会附加一个内容哈希。这对于从 URL 或 CDN 提供文件并减少缓存失效问题的情况很有用。但有时这可能出乎意料,您可能更希望使用原始名称

要禁用内容哈希,请向 bun build --compile 传递 --asset-naming 参数,如下所示

bun build --compile --asset-naming="[name].[ext]" ./index.ts

代码压缩

要稍微减小可执行文件的大小,请向 bun build --compile 传递 --minify 参数。这将使用 Bun 的压缩器来减小代码大小。总的来说,Bun 的二进制文件仍然太大了,我们需要进一步减小它。

使用 Bun.build() API

您还可以使用 Bun.build() JavaScript API 来生成独立的可执行文件。当您需要对构建过程进行编程控制时,这非常有用。

基本用法

await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  compile: {
    target: "bun-windows-x64",
    outfile: "myapp.exe",
  },
});

使用 Bun.build() 添加 Windows 元数据

当目标平台为 Windows 时,您可以通过 windows 对象指定元数据

await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  compile: {
    target: "bun-windows-x64",
    outfile: "myapp.exe",
    windows: {
      title: "My Application",
      publisher: "My Company Inc",
      version: "1.2.3.4",
      description: "A powerful application built with Bun",
      copyright: "© 2024 My Company Inc",
      hideConsole: false, // Set to true for GUI applications
      icon: "./icon.ico", // Path to icon file
    },
  },
});

使用 Bun.build() 进行交叉编译

您可以为不同的平台进行交叉编译

// Build for multiple platforms
const platforms = [
  { target: "bun-windows-x64", outfile: "app-windows.exe" },
  { target: "bun-linux-x64", outfile: "app-linux" },
  { target: "bun-darwin-arm64", outfile: "app-macos" },
];

for (const platform of platforms) {
  await Bun.build({
    entrypoints: ["./app.ts"],
    outdir: "./dist",
    compile: platform,
  });
}

Windows 特定标志

在编译 Windows 的独立可执行文件时,有几个特定于平台的选项可用于自定义生成的 .exe 文件

视觉自定义

  • --windows-icon=path/to/icon.ico - 设置可执行文件图标
  • --windows-hide-console - 禁用后台终端窗口(适用于 GUI 应用程序)

元数据自定义

您可以将版本信息和其他元数据嵌入到您的 Windows 可执行文件中

  • --windows-title <STR> - 设置产品名称(显示在文件属性中)
  • --windows-publisher <STR> - 设置公司名称
  • --windows-version <STR> - 设置版本号(例如“1.2.3.4”)
  • --windows-description <STR> - 设置文件描述
  • --windows-copyright <STR> - 设置版权信息

包含所有元数据标志的示例

bun build --compile ./app.ts \
  --outfile myapp.exe \
  --windows-title "My Application" \
  --windows-publisher "My Company Inc" \
  --windows-version "1.2.3.4" \
  --windows-description "A powerful application built with Bun" \
  --windows-copyright "© 2024 My Company Inc"

这些元数据将在 Windows Explorer 中查看文件属性时可见

  1. 在 Windows Explorer 中右键单击可执行文件
  2. 选择“属性”
  3. 转到“详细信息”选项卡

版本字符串格式

--windows-version 标志接受以下格式的版本字符串

  • "1" - 将被规范化为“1.0.0.0”
  • "1.2" - 将被规范化为“1.2.0.0”
  • "1.2.3" - 将被规范化为“1.2.3.0”
  • "1.2.3.4" - 完整版本格式

每个版本组件必须是介于 0 和 65535 之间的数字。

由于依赖 Windows API,这些标志目前无法在交叉编译时使用。它们仅在 Windows 本身构建时可用。

macOS 代码签名

要在 macOS 上对独立可执行文件进行代码签名(这可以修复 Gatekeeper 警告),请使用 codesign 命令。

codesign --deep --force -vvvv --sign "XXXXXXXXXX" ./myapp

我们建议包含一个带有 JIT 权限的 entitlements.plist 文件。

entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
</dict>
</plist>

要使用 JIT 支持进行代码签名,请向 codesign 传递 --entitlements 标志。

codesign --deep --force -vvvv --sign "XXXXXXXXXX" --entitlements entitlements.plist ./myapp

代码签名后,验证可执行文件

codesign -vvv --verify ./myapp
./myapp: valid on disk
./myapp: satisfies its Designated Requirement

代码签名支持需要 Bun v1.2.4 或更高版本。

代码分割

独立可执行文件支持代码分割。使用 --compile--splitting 来创建一个在运行时加载代码分割块的可执行文件。

bun build --compile --splitting ./src/entry.ts --outdir ./build
src/entry.ts
src/lazy.ts
src/entry.ts
console.log("Entrypoint loaded");
const lazy = await import("./lazy.ts");
lazy.hello();
src/lazy.ts
export function hello() {
  console.log("Lazy module loaded");
}
./build/entry
Entrypoint loaded
Lazy module loaded

不支持的 CLI 参数

目前,--compile 标志一次只能接受一个入口点,并且不支持以下标志

  • --outdir — 请改用 outfile(除非与 --splitting 一起使用)。
  • --public-path
  • --target=node--target=browser
  • --no-bundle - 我们总是将所有内容打包到可执行文件中。