Bun

Bun 打包器


Jarred Sumner · 2023 年 5 月 16 日

Bun 快速的原生打包器现在处于 beta 测试阶段。它可以通过 bun build CLI 命令或新的 Bun.build() JavaScript API 使用。

从头开始打包 10 份 three.js,带有 sourcemap 和压缩

使用打包器通过内置的 Bun.build() 函数或 bun build CLI 命令来构建前端应用程序。

JavaScript
CLI
JavaScript
Bun.build({
  entrypoints: ['./src/index.tsx'],
  outdir: './build',
  minify: true,
  // additional config
});
CLI
bun build ./src/index.tsx --outdir ./build --minify

降低 JavaScript 的复杂性

JavaScript 最初是表单字段的自动填充,而今天它为将火箭发射到太空的仪器提供动力。

毫不奇怪,JavaScript 生态系统的复杂性呈爆炸式增长。如何运行 TypeScript 文件?如何构建/打包你的代码以用于生产环境?该软件包是否与 ESM 兼容?如何加载本地专用配置?我需要安装对等依赖项吗?我如何让 sourcemap 工作?

复杂性会耗费时间,通常花费在粘合工具或等待事情完成上。安装 npm 包花费的时间太长。运行测试应该只需要几秒钟(或更短)。当 2003 年将文件上传到 FTP 服务器只需几毫秒时,为什么 2023 年部署软件却需要几分钟?

多年来,我对 JavaScript 周围的一切都如此缓慢感到沮丧。当从保存文件到测试更改的迭代周期时间变得足够长,以至于本能地查看 Hacker News 时,就说明有问题了。

复杂性是有充分理由的。打包器和压缩器使网站加载速度更快。TypeScript 的编辑器内交互式文档使开发人员更有效率。类型安全有助于在错误发布给用户之前捕获它们。依赖项作为版本控制的软件包通常比复制文件更容易维护。

当“一件事情”被拆分到如此多孤立的工具之间时,“做好一件事”的 Unix 哲学就崩溃了。

这就是我们构建 Bun 的原因,也是为什么今天我们很高兴推出 Bun 打包器的原因。

是的,一个新的打包器

借助新的打包器,打包现在成为 Bun 生态系统的首要元素,完整配备了 bun build CLI 命令、新的顶级 Bun.build 函数和稳定的插件系统。

我们决定 Bun 需要自己的打包器有几个原因。

内聚性

打包器是编排和启用所有其他工具的元工具,例如 JSX、TypeScript、CSS 模块和服务器组件——所有这些都需要打包器集成才能工作。

如今,打包器是 JavaScript 生态系统中巨大复杂性的来源。通过将打包引入 JavaScript 运行时,我们认为我们可以使交付前端和全栈代码更简单、更快速。

  • 快速插件。 插件在轻量级的 Bun 进程中执行,该进程启动速度很快。
  • 没有冗余的转译。使用 target: "bun",打包器生成针对 Bun 运行时优化的预编译文件,从而提高运行性能并避免不必要的重新转译。
  • 统一的插件 API。Bun 提供了一个统一的插件 API,该 API 既适用于打包器适用于运行时。任何扩展 Bun 打包功能的插件也可以用于扩展 Bun 的运行时功能。
  • 运行时集成。构建返回 BuildArtifact 对象数组,这些对象实现了 Blob 接口,可以直接传递到 HTTP API 中,例如 new Response()。运行时为 BuildArtifact 实现了特殊的漂亮打印。
  • 独立可执行文件。打包器可以通过 --compile 标志从 TypeScript 和 JavaScript 脚本生成独立的可执行文件。这些可执行文件完全是独立的,并包含 Bun 运行时的副本。

很快,打包器将与 Bun 的 HTTP 服务器 API (Bun.serve) 集成,从而可以使用简单的声明式 API 来表示当前复杂的构建管道。稍后会详细介绍。

性能

这一点不会让任何人感到惊讶。作为运行时,Bun 的代码库已经包含了快速解析和转换源代码的基础(用 Zig 实现)。虽然有可能,但很难与现有的原生打包器集成,并且进程间通信中涉及的开销会损害性能。

最终结果不言自明。在 我们的基准测试(源自 esbuild 的 three.js 基准测试)中,Bun 比 esbuild 快 1.75 倍,比 Parcel 2 快 150 倍,比 Rollup + Terser 快 180 倍,比 Webpack 快 220 倍。

开发者体验

查看现有打包器的 API,我们看到了很大的改进空间。没有人喜欢与打包器配置作斗争。Bun 的打包器 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 };
}

其他打包器为了追求功能完整性而做出了糟糕的架构决策,最终削弱了性能;这是我们正在努力避免的错误。

模块系统

目前仅支持 format: "esm"。我们计划添加对其他模块系统和目标(如 iife)的支持。如果足够多的人要求,我们也将添加 cjs 输出支持(支持 CommonJS 输入,但不支持输出)。

目标

支持三个“目标”:"browser"(默认)、"bun""node"

browser

  • TypeScript 和 JSX 会自动转译为 vanilla JavaScript。
  • 当可用时,模块使用 "browser" package.json "exports" 条件解析
  • 当在浏览器中导入时,Bun 会自动 polyfill 某些 Node.js API,例如 node:crypto,类似于 Webpack 4 的行为。Bun 自己的 API 目前被禁止导入,但我们将来可能会重新考虑这一点。

bun

  • 支持 Bun 和 Node.js API,并保持不变。
  • 模块使用 Bun 运行时使用的默认解析算法解析。
  • 生成的捆绑包标有特殊的 // @bun pragma 注释,以指示它们是由 Bun 生成的。这向 Bun 的运行时表明该文件在执行前不需要重新转译。协同作用!

node

目前,这与 target: "bun" 相同。将来,我们计划自动 polyfill Bun API,例如 Bun 全局变量和 bun:* 内置模块。

文件类型

打包器支持以下文件类型

  • .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);

插件

与运行时本身一样,打包器旨在通过插件进行扩展。实际上,运行时插件和打包器插件之间没有任何区别。

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 数组包含构建生成的所有文件。每个工件都实现了 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
}

工件还包含以下附加属性

kind此文件是哪种构建输出。构建生成捆绑的入口点、代码拆分的“块”、sourcemap 和复制的资产(如图像)。
path磁盘上文件的绝对路径,如果文件未写入磁盘,则为输出路径。
loader用于解释文件的加载器。请参阅 打包器 > 加载器 以了解 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);

当将 BuildArtifact 对象记录到日志中时,Bun 运行时实现了特殊的漂亮打印,以使调试更容易。

构建脚本
Shell 输出
构建脚本
// build.ts
const build = Bun.build({/* */});

const artifact = build.outputs[0];
console.log(artifact);
Shell 输出
bun run build.ts
BuildArtifact (entry-point) {
  path: "./index.js",
  loader: "tsx",
  kind: "entry-point",
  hash: "824a039620219640",
  Blob (114 bytes) {
    type: "text/javascript;charset=utf-8"
  },
  sourcemap: null
}

服务器组件

Bun 的打包器通过 --server-components 标志对 React 服务器组件提供实验性支持。我们将在本周晚些时候发布更多文档和一个示例项目。

Tree shaking

Bun 的打包器支持 tree-shaking 未使用的代码。在打包时始终启用此功能。

package.json "sideEffects" 字段

Bun 在 package.json 中支持 "sideEffects": false。这是给打包器的提示,表明该软件包没有副作用,并且可以更积极地消除死代码。

__PURE__ 注释

Bun 支持 __PURE__ 注释

file.js
file.js
function foo() {
  return 123;
}

/** #__PURE__ */ foo();

由于 foo 是无副作用的,因此会导致一个空文件

output.js

Webpack 的文档中了解更多信息。

process.env.NODE_ENV--define

Bun 支持 NODE_ENV 环境变量和 --define CLI 标志。这些通常用于有条件地在生产版本中包含代码。

如果 process.env.NODE_ENV 设置为 "production",Bun 将自动删除包装在 if (process.env.NODE_ENV !== "production") { ... } 中的代码。

node-env.js
node-env.js
if (process.env.NODE_ENV !== "production") {
  module.exports = require("./cjs/react.development.js");
} else {
  module.exports = require("./cjs/react.production.min.js");
}

ES 模块 tree-shaking

ESM tree-shaking 同时支持 ESM 和 CommonJS 输入文件。当安全时,Bun 的打包器将自动从 ESM 文件中删除未使用的导出。

entry.js
foo.js
entry.js
import { foo } from "./foo.js";
console.log(foo);
foo.js
export const bar = 123;
export const foo = 456;

未使用的 bar 导出被消除,导致

output.js
output.js
// foo.js
var $foo = 456;
console.log($foo);

CommonJS tree-shaking

在有限的情况下,Bun 的打包器会自动将 CommonJS 转换为 ESM,而运行时开销为零。考虑这个简单的例子

index.ts
foo.js
index.ts
import { foo } from "./foo.js";
console.log(foo);
foo.js
// foo.js
exports.foo = 123;

exports.bar = "this will be treeshaken";

Bun 将自动将 foo.js 转换为 ESM,并 tree-shake 未使用的 exports 对象。

已打包
// foo.js
var $foo = 123;

// entry.js
console.log($foo);

请注意,在许多情况下,CommonJS 的动态性质使这非常困难。例如,考虑以下三个文件

entry.js
foo.js
bar.js
entry.js
// entry.js
export default require("./foo");
foo.js
// foo.js
exports.foo = 123;
Object.assign(module.exports, require("./bar"));
bar.js
// bar.js
exports.foobar = 123;

Bun 无法在不执行 foo.js 的情况下静态确定其导出。(此外,Object.assign 可以被覆盖,这使得在一般情况下静态分析变得不可能。)在这种情况下,Bun 将不会 tree-shake exports 对象;相反,它会注入一些 CommonJS 运行时代码以使其按预期工作。

CommonJS 包装器

Sourcemap

打包器支持内联和外部 sourcemap。

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 打包器是不完整的。此版本还引入了 Bun 内置的全新 JavaScript minifier。使用 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

打包器只是为一项更雄心勃勃的工作奠定基础。在接下来的几个月中,我们将宣布 Bun.App:一个“超级 API”,它将 Bun 的原生速度打包器、HTTP 服务器和文件系统路由器缝合在一起,形成一个有凝聚力的整体。

目标是使使用几行代码即可轻松表达任何类型的 Bun 应用程序

静态文件服务器
API 服务器
Next.js 风格的框架
静态文件服务器
new Bun.App({
 bundlers: [
   {
     name: "static-server",
     outdir: "./out",
   },
 ],
 routers: [
   {
     mode: "static",
     dir: "./public",
     build: "static-server",
   },
 ],
});

app.serve();
app.build();
API 服务器
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();
Next.js 风格的框架
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 打包器和 minifier 的架构基于 esbuild 的设计,因此感谢 Evan Wallace (evanw)。
  • 感谢 @paperclover 将 esbuild 的测试套件移植到 Bun。
  • 感谢 @dylan-conway 实现 sourcemap 支持并修复了许多错误。