Bun 快速的原生 bundler 现已进入 beta 测试阶段。您可以通过 bun build 命令行命令或新的 Bun.build() JavaScript API 来使用它。

使用 bundler 通过内置的 Bun.build() 函数或 bun build 命令行命令来构建前端应用程序。
Bun.build({
entrypoints: ['./src/index.tsx'],
outdir: './build',
minify: true,
// additional config
});
bun build ./src/index.tsx --outdir ./build --minify降低 JavaScript 的复杂性
JavaScript 最初是为了给表单字段自动填充而设计的,而如今它为驱动火箭发射到太空的仪器提供支持。
毫不意外,JavaScript 生态系统经历了爆炸性的复杂化。如何运行 TypeScript 文件?如何为生产环境构建/打包你的代码?那个包是否与 ESM 兼容?如何加载仅本地存在的配置?我需要安装对等依赖项吗?如何让 sourcemaps 工作?
复杂性会消耗时间,通常用于粘合各种工具或等待其完成。安装 npm 包耗时太长。运行测试应该需要几秒钟(或更短)。为什么在 2023 年部署软件需要几分钟,而 2003 年上传文件到 FTP 服务器只需几毫秒?
多年来,我一直对 JavaScript 周围的一切有多慢感到沮丧。当从保存文件到测试更改的迭代周期变得足够长,以至于你会本能地去查看 Hacker News 时,就说明有问题了。
这种复杂性有其合理的原因。Bundler 和 minifier 使网站加载速度更快。TypeScript 的编辑器内交互式文档使开发人员更有效率。类型安全有助于在用户看到 bug 之前捕获它们。作为版本化包的依赖项通常比复制文件更容易维护。
“做好一件事”的 Unix 哲学在“一件事”被分割到如此多的独立工具时就失效了。
这就是我们构建 Bun 的原因,也是为什么我们今天很高兴推出 Bun bundler。
是的,一个新的 bundler
有了新的 bundler,打包成为 Bun 生态系统的第一类元素,它包括一个 bun build 命令行命令、一个新的顶级 Bun.build 函数以及一个稳定的插件系统。
我们决定 Bun 需要自己的 bundler,有几个原因。
凝聚性
Bundler 是一个元工具,它协调并启用所有其他工具,如 JSX、TypeScript、CSS 模块和服务器组件——所有这些都需要 bundler 集成才能工作。
今天,bundler 是 JavaScript 生态系统中巨大复杂性的一个来源。通过将打包引入 JavaScript 运行时,我们认为可以使发布前端和全栈代码更简单、更快。
- 快速的插件。插件在一个启动快速的轻量级 Bun 进程中执行。
- 无冗余转译。使用
target: "bun",bundler 会生成为 Bun 运行时优化的预转译文件,提高运行性能并避免不必要的重复转译。 - 统一的插件 API。Bun 提供了一个统一的插件 API,该 API 同时适用于 bundler 和运行时。任何扩展 Bun 打包功能的插件也可以用来扩展 Bun 的运行时功能。
- 运行时集成。构建返回一个
BuildArtifact对象数组,这些对象实现了Blob接口,可以直接传递给 HTTP API,如new Response()。运行时为BuildArtifact实现特殊的漂亮打印。 - 独立的可执行文件。bundler 可以通过
--compile标志从 TypeScript 和 JavaScript 脚本生成独立的可执行文件。这些可执行文件是完全独立的,并包含 Bun 运行时的副本。
很快,bundler 将与 Bun 的 HTTP 服务器 API (Bun.serve) 集成,使得可以通过简单的声明式 API 来表示目前复杂的构建管道。更多内容稍后详述。
性能
这一点应该不会让任何人感到意外。作为一个运行时,Bun 的代码库已经包含了(用 Zig 实现的)用于快速解析和转换源代码的基础。虽然可能,但将其与现有的原生 bundler 集成将非常困难,而且进程间通信的开销会影响性能。
最终结果会说明一切。在我们的基准测试(该测试源自 esbuild 的 three.js benchmark)中,Bun 的速度是 esbuild 的 1.75 倍,Parcel 2 的 150 倍,Rollup + Terser 的 180 倍,以及 Webpack 的 220 倍。
开发者体验
查看现有 bundler 的 API,我们发现有很多改进的空间。没有人喜欢与 bundler 配置作斗争。Bun 的 bundler API 设计得清晰明了,并且符合预期。说到这个……
API
API 目前设计上是最小化的。我们这个初始版本的目标是实现一个最小化的功能集,使其快速、稳定,并在不牺牲性能的情况下适应大多数现代用例。
这是 API 目前存在的形式
interface Bun {
build(options: BuildOptions): Promise<BuildOutput>;
}
interface BuildOptions {
entrypoints: string[]; // required
outdir?: string; // default: no write (in-memory only)
target?: "browser" | "bun" | "node"; // "browser"
format?: "esm"; // later: "cjs" | "iife"
splitting?: boolean; // default false
plugins?: BunPlugin[]; // [] // see https://bun.net.cn/docs/bundler/plugins
loader?: { [k in string]: string }; // see https://bun.net.cn/docs/bundler/loaders
external?: string[]; // default []
sourcemap?: "none" | "inline" | "external"; // default "none"
root?: string; // default: computed from entrypoints
publicPath?: string; // e.g. http://mydomain.com/
naming?:
| string // equivalent to naming.entry
| { entry?: string; chunk?: string; asset?: string };
minify?:
| boolean // default false
| { identifiers?: boolean; whitespace?: boolean; syntax?: boolean };
}
其他 bundler 为了实现功能完整性而做出了糟糕的架构决策,最终损害了性能;这是我们正努力避免的错误。
模块系统
目前仅支持 format: "esm"。我们计划添加对其他模块系统和目标的支持,例如 iife。如果足够多人提出要求,我们也会添加 cjs 输出支持(CommonJS 输入是支持的,但输出不支持)。
目标
支持三个“目标”:“browser”(默认)、“bun”和“node”。
浏览器
- TypeScript 和 JSX 会自动转译成原生 JavaScript。
- 当可用时,模块将使用
package.json的"exports"条件中的"browser"进行解析。 - Bun 在浏览器中导入时会自动为某些 Node.js API 提供 polyfill,例如
node:crypto,类似于 Webpack 4 的行为。目前 Bun 的 API 本身禁止被导入,但我们将来可能会重新考虑这一点。
bun
- 支持 Bun 和 Node.js API,且保持不变。
- 模块使用 Bun 运行时的默认解析算法进行解析。
- 生成的 bundle 会有一个特殊的
// @bunpragma 注释,表明它们是由 Bun 生成的。这告诉 Bun 运行时文件在执行前不需要重新转译。协同效应!
node
目前,这与 target: "bun" 完全相同。将来,我们计划为 Bun API(如 Bun 全局和 bun:* 内置模块)提供自动 polyfill。
文件类型
bundler 支持以下文件类型:
.js.jsx.ts.tsx- JavaScript 和 TypeScript 文件。当然。.txt— 纯文本文件。这些文件会被内联为字符串。.json.toml— 这些文件会在编译时解析并内联为 JSON。
其他所有内容都被视为资产。资产会被原样复制到 outdir,并且导入会被替换为指向文件的相对路径或 URL,例如 /images/logo.png。
import logo from "./images/logo.png";
console.log(logo);
var logo = "./images/logo.png";
console.log(logo);
插件
与运行时本身一样,bundler 设计为可通过插件进行扩展。实际上,运行时插件和 bundler 插件之间没有任何区别。
import YamlPlugin from "bun-plugin-yaml";
const plugin = YamlPlugin();
// register a runtime plugin
Bun.plugin(plugin);
// register a bundler plugin
Bun.build({
entrypoints: ["./src/index.ts"],
plugins: [plugin],
});
构建输出
Bun.build 函数返回一个 Promise<BuildOutput>,定义如下:
interface BuildOutput {
outputs: BuildArtifact[];
success: boolean;
logs: Array<object>; // see docs for details
}
interface BuildArtifact extends Blob {
kind: "entry-point" | "chunk" | "asset" | "sourcemap";
path: string;
loader: Loader;
hash: string | null;
sourcemap: BuildArtifact | null;
}
outputs 数组包含构建生成的所有文件。每个 artifact 都实现了 Blob 接口。
const build = await Bun.build({
/* */
});
for (const output of build.outputs) {
output.size; // file size in bytes
output.type; // MIME type of file
await output.arrayBuffer(); // => ArrayBuffer
await output.text(); // string
}
Artifact 还包含以下附加属性:
kind | 这个文件是哪种类型的构建输出。一个构建会生成打包好的入口点、代码分割的“块”、sourcemaps 和复制的资产(如图像)。 |
path | 文件在磁盘上的绝对路径,或者如果文件未写入磁盘,则为输出路径。 |
loader | 用于解释文件的加载器。请参阅 Bundler > Loaders,了解 Bun 如何将文件扩展名映射到相应的内置加载器。 |
hash | 文件内容的哈希值。对于资产始终定义。 |
sourcemap | 对应于此文件的 sourcemap 的另一个 BuildArtifact,如果已生成。仅为入口点和块定义。 |
与 BunFile 类似,BuildArtifact 对象可以直接传递到 new Response() 中。
const build = Bun.build({
/* */
});
const artifact = build.outputs[0];
// Content-Type is set automatically
return new Response(artifact);
Bun 运行时实现了特殊的漂亮打印功能,用于在日志记录 BuildArtifact 对象时使其更容易调试。
// build.ts
const build = Bun.build({/* */});
const artifact = build.outputs[0];
console.log(artifact);
bun run build.tsBuildArtifact (entry-point) {
path: "./index.js",
loader: "tsx",
kind: "entry-point",
hash: "824a039620219640",
Blob (114 bytes) {
type: "text/javascript;charset=utf-8"
},
sourcemap: null
}服务器组件
Bun 的 bundler 对 React 服务器组件具有实验性支持,通过 --server-components 标志。我们将在本周晚些时候发布更多文档和一个示例项目。
树摇
Bun 的 bundler 支持对未使用的代码进行树摇。这在打包时始终启用。
package.json "sideEffects" 字段
Bun 支持 package.json 中的 "sideEffects": false。这是 bundler 的一个提示,表明该包没有副作用,并且可以实现更积极的死代码消除。
__PURE__ 注释
Bun 支持 __PURE__ 注释。
function foo() {
return 123;
}
/** #__PURE__ */ foo();
由于 foo 是无副作用的,因此结果是一个空文件。
在 Webpack 的文档中了解更多信息。
process.env.NODE_ENV 和 --define
Bun 支持 NODE_ENV 环境变量和 --define CLI 标志。这些通常用于在生产构建中条件性地包含代码。
如果 process.env.NODE_ENV 设置为 "production",Bun 将自动移除用 if (process.env.NODE_ENV !== "production") { ... } 包裹的代码。
if (process.env.NODE_ENV !== "production") {
module.exports = require("./cjs/react.development.js");
} else {
module.exports = require("./cjs/react.production.min.js");
}
ES 模块树摇
ESM 树摇支持 ESM 和 CommonJS 输入文件。当安全时,Bun 的 bundler 会自动移除 ESM 文件中未使用的导出。
import { foo } from "./foo.js";
console.log(foo);
export const bar = 123;
export const foo = 456;
未使用的 bar 导出被消除,结果是:
// foo.js
var $foo = 456;
console.log($foo);
CommonJS 树摇
在有限的情况下,Bun 的 bundler 会以零运行时开销自动将 CommonJS 转换为 ESM。考虑这个简单的例子:
import { foo } from "./foo.js";
console.log(foo);
// foo.js
exports.foo = 123;
exports.bar = "this will be treeshaken";
Bun 将自动转换 foo.js 为 ESM 并进行树摇,消除未使用的 exports 对象。
// foo.js
var $foo = 123;
// entry.js
console.log($foo);
请注意,CommonJS 的动态特性在许多情况下使其非常难以处理。例如,考虑以下三个文件:
// entry.js
export default require("./foo");
// foo.js
exports.foo = 123;
Object.assign(module.exports, require("./bar"));
// bar.js
exports.foobar = 123;
Bun 在不执行 foo.js 的情况下无法静态确定其导出。(另外 Object.assign 可以被覆盖,这使得在一般情况下静态分析不可能。)在这种情况下,Bun 不会进行 exports 对象的树摇;相反,它会注入一些 CommonJS 运行时代码使其按预期工作。
CommonJS wrapper
Source maps
bundler 支持内联和外部 source maps。
const build = await Bun.build({
entrypoints: ["./src/index.ts"],
// generates a *.js.map file alongside each output
sourcemap: "external",
// adds a base64-encoded `sourceMappingURL` to the end of each output file
sourcemap: "inline",
});
console.log(await build.outputs[0].sourcemap.json()); // => { version: 3, ... }
Minifier
没有 minifier 的 JavaScript bundler 是不完整的。此版本还引入了一个全新的 JavaScript minifier,内置于 Bun 中。使用 minify: true 启用最小化,或使用以下选项更精细地配置最小化行为:
{
minify?: boolean | {
identifiers?: boolean; // default: false
whitespace?: boolean; // default: false
syntax?: boolean; // default: false
}
}
minifier 能够移除死代码、重命名标识符、移除空格以及智能地压缩和内联常量值。
// This comment will be removed!
console.log("this" + " " + "text" + " will" + " be " + "merged");
console.log("this text will be merged");
抢先看:Bun.App
bundler 只是为一项更宏大的计划奠定基础。在接下来的几个月里,我们将宣布 Bun.App:一个“超级 API”,它将 Bun 的原生速度 bundler、HTTP 服务器和文件系统路由缝合在一起,形成一个统一的整体。
目标是让用几行代码就能轻松表达任何类型的 Bun 应用。
new Bun.App({
bundlers: [
{
name: "static-server",
outdir: "./out",
},
],
routers: [
{
mode: "static",
dir: "./public",
build: "static-server",
},
],
});
app.serve();
app.build();
const app = new Bun.App({
configs: [
{
name: "simple-http",
target: "bun",
outdir: "./.build/server",
// bundler config...
},
],
routers: [
{
mode: "handler",
handler: "./handler.tsx", // automatically included as entrypoint
prefix: "/api",
build: "simple-http",
},
],
});
app.serve();
app.build();
const projectRoot = process.cwd();
const app = new Bun.App({
configs: [
{
name: "react-ssr",
target: "bun",
outdir: "./.build/server",
// bundler config
},
{
name: "react-client",
target: "browser",
outdir: "./.build/client",
transform: {
exports: {
pick: ["default"],
},
},
},
],
routers: [
{
mode: "handler",
handler: "./handler.tsx",
build: "react-ssr",
style: "nextjs",
dir: projectRoot + "/pages",
},
{
mode: "build",
build: "react-client",
dir: "./pages",
// style: "build",
// dir: projectRoot + "/pages",
prefix: "_pages",
},
],
});
app.serve();
app.build();
此 API 仍处于 积极讨论中,并可能发生变更。
致谢
- bun bundler 和 minifier 的架构基于 esbuild 的设计,因此感谢 Evan Wallace (evanw)。
- 感谢 @paperclover 将 esbuild 的测试套件移植到 Bun。
- 感谢 @dylan-conway 实现 source maps 支持并修复了大量 bug。