Bun

Bun.build

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

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './build',
});
CLI
bun build ./index.tsx --outdir ./build

它很快。以下数字代表 esbuild 的性能three.js 基准测试

从头开始捆绑 10 份 three.js,包括源映射和缩小

为什么捆绑?

捆绑器是 JavaScript 生态系统中基础设施的关键部分。作为捆绑如此重要的原因的简要概述

  • 减少 HTTP 请求。node_modules 中的单个包可能包含数百个文件,大型应用程序可能具有数十个这样的依赖项。使用单独的 HTTP 请求加载这些每个文件很快就会变得不可行,因此捆绑器用于将我们的应用程序源代码转换为数量较少的自包含“捆绑包”,这些捆绑包可以通过单个请求加载。
  • 代码转换。现代应用程序通常使用 TypeScript、JSX 和 CSS 模块等语言或工具构建,所有这些都必须转换为纯 JavaScript 和 CSS,然后才能被浏览器使用。打包器是配置这些转换的自然场所。
  • 框架特性。框架依赖于打包器插件和代码转换来实现常见模式,如文件系统路由、客户端-服务器代码并置(想想 getServerSideProps 或 Remix 加载器)以及服务器组件。

让我们深入了解打包器 API。

请注意,Bun 打包器并非旨在替换 tsc 以进行类型检查或生成类型声明。

基本示例

让我们构建我们的第一个包。您有以下两个文件,它们实现了一个简单的客户端渲染 React 应用程序。

./index.tsx
./Component.tsx
./index.tsx
import * as ReactDOM from 'react-dom/client';
import {Component} from "./Component"

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<Component message="Sup!" />)
./Component.tsx
export function Component(props: {message: string}) {
  return <p>{props.message}</p>
}

这里,index.tsx 是我们应用程序的“入口点”。通常,这将是一个执行某些副作用的脚本,比如启动服务器或——在这种情况下——初始化 React 根。由于我们使用 TypeScript 和 JSX,我们需要在将代码发送到浏览器之前对其进行打包。

要创建我们的包

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
})
CLI
bun build ./index.tsx --outdir ./out

对于 entrypoints 中指定的每个文件,Bun 将生成一个新包。此包将写入到 ./out 目录中(从当前工作目录解析)。在运行构建后,文件系统看起来像这样

.
├── index.tsx
├── Component.tsx
└── out
    └── index.js

out/index.js 的内容看起来像这样

out/index.js
// ...
// ~20k lines of code
// including the contents of `react-dom/client` and all its dependencies
// this is where the $jsxDEV and $createRoot functions are defined


// Component.tsx
function Component(props) {
  return $jsxDEV("p", {
    children: props.message
  }, undefined, false, undefined, this);
}

// index.tsx
var rootNode = document.getElementById("root");
var root = $createRoot(rootNode);
root.render($jsxDEV(Component, {
  message: "Sup!"
}, undefined, false, undefined, this));

教程:在浏览器中运行此文件

监视模式

与运行时和测试运行器一样,打包器本机支持监视模式。

bun build ./index.tsx --outdir ./out --watch

内容类型

与 Bun 运行时一样,打包器开箱即用地支持一系列文件类型。下表细分了打包器的标准“加载器”集。有关完整文档,请参阅 打包器 > 文件类型

扩展名详细信息
.js .jsx, .cjs .mjs .mts .cts .ts .tsx使用 Bun 的内置编译器解析文件,并将 TypeScript/JSX 语法编译为原生 JavaScript。捆绑器执行一组默认转换,包括死代码消除和摇树。目前,Bun 不会尝试向下转换语法;如果你使用最近的 ECMAScript 语法,它将反映在捆绑代码中。

.json

JSON 文件被解析并作为 JavaScript 对象内联到捆绑包中。

import pkg from "./package.json";
pkg.name; // => "my-package"

.toml

TOML 文件被解析并作为 JavaScript 对象内联到捆绑包中。

import config from "./bunfig.toml";
config.logLevel; // => "debug"

.txt

文本文件的内容被读取并作为字符串内联到捆绑包中。

import contents from "./file.txt";
console.log(contents); // => "Hello, world!"
.node .wasm这些文件受 Bun 运行时支持,但在捆绑期间它们被视为 资源

资源

如果捆绑器遇到具有无法识别的扩展名的导入,它会将导入的文件视为外部文件。引用的文件按原样复制到 outdir 中,并且导入被解析为文件的路径

输入
输出
输入
// bundle entrypoint
import logo from "./logo.svg";
console.log(logo);
输出
// bundled output
var logo = "./logo-ab237dfe.svg";
console.log(logo);

文件加载器的确切行为也受 namingpublicPath 的影响。

请参阅 捆绑器 > 加载器 页面,以获取有关文件加载器的更完整文档。

插件

此表中描述的行为可以通过 插件 覆盖或扩展。请参阅 捆绑器 > 加载器 页面以获取完整文档。

API

entrypoints

必需。与我们应用程序的入口点相对应的路径数组。将为每个入口点生成一个捆绑包。

JavaScript
CLI
JavaScript
const result = await Bun.build({
  entrypoints: ["./index.ts"],
});
// => { success: boolean, outputs: BuildArtifact[], logs: BuildMessage[] }
CLI
bun build --entrypoints ./index.ts
# the bundle will be printed to stdout
# <bundled code>

outdir

输出文件将被写入的目录。

JavaScript
CLI
JavaScript
const result = await Bun.build({
  entrypoints: ['./index.ts'],
  outdir: './out'
});
// => { success: boolean, outputs: BuildArtifact[], logs: BuildMessage[] }
CLI
bun build --entrypoints ./index.ts --outdir ./out
# a summary of bundled files will be printed to stdout

如果 outdir 未传递给 JavaScript API,则捆绑代码将不会写入磁盘。捆绑文件以 BuildArtifact 对象数组的形式返回。这些对象是带有额外属性的 Blob;请参阅 输出 以获取完整文档。

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

for (const res of result.outputs) {
  // Can be consumed as blobs
  await res.text();

  // Bun will set Content-Type and Etag headers
  new Response(res);

  // Can be written manually, but you should use `outdir` in this case.
  Bun.write(path.join("out", res.path), res);
}

当设置 outdir 时,BuildArtifact 上的 path 属性将是写入它的绝对路径。

target

捆绑包的目标执行环境。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.ts'],
  outdir: './out',
  target: 'browser', // default
})
CLI
bun build --entrypoints ./index.ts --outdir ./out --target browser

根据目标,Bun 将应用不同的模块解析规则和优化。

browser默认。用于生成旨在由浏览器执行的捆绑包。解析导入时优先考虑 "browser" 导出条件。导入任何内置模块(如 node:eventsnode:path)将起作用,但调用某些函数(如 fs.readFile)将不起作用。

bun

用于生成旨在由 Bun 运行时运行的捆绑包。在许多情况下,无需捆绑服务器端代码;您可以直接执行源代码,无需修改。但是,捆绑服务器代码可以减少启动时间并提高运行性能。

所有使用 target: "bun" 生成的捆绑包都标记有特殊 // @bun 编译指示,这向 Bun 运行时表明在执行之前无需重新编译文件。

如果任何入口点包含 Bun shebang (#!/usr/bin/env bun),则捆绑器将默认使用 target: "bun" 而不是 "browser"

node用于生成旨在由 Node.js 运行的捆绑包。解析导入时优先考虑 "node" 导出条件,并输出 .mjs。将来,这将自动填充 Bun 全局和其他内置 bun:* 模块,尽管这尚未实现。

format

指定要在生成的捆绑包中使用的模块格式。

目前,捆绑器仅支持一种模块格式:"esm"。计划支持 "cjs""iife"

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  format: "esm",
})
CLI
bun build ./index.tsx --outdir ./out --format esm

splitting

是否启用代码拆分。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  splitting: false, // default
})
CLI
bun build ./index.tsx --outdir ./out --splitting

true 时,打包器将启用代码拆分。当多个入口点都导入相同的文件、模块或文件/模块集时,通常将共享代码拆分为一个单独的包很有用。此共享包称为。考虑以下文件

entry-a.ts
entry-b.ts
shared.ts
entry-a.ts
import { shared } from './shared.ts';
entry-b.ts
import { shared } from './shared.ts';
shared.ts
export const shared = 'shared';

要使用启用了代码拆分的 entry-a.tsentry-b.ts 进行打包

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./entry-a.ts', './entry-b.ts'],
  outdir: './out',
  splitting: true,
})
CLI
bun build ./entry-a.ts ./entry-b.ts --outdir ./out --splitting

运行此构建将生成以下文件

.
├── entry-a.tsx
├── entry-b.tsx
├── shared.tsx
└── out
    ├── entry-a.js
    ├── entry-b.js
    └── chunk-2fce6291bf86559d.js

生成的 chunk-2fce6291bf86559d.js 文件包含共享代码。为了避免冲突,默认情况下文件名自动包含内容哈希。这可以通过 naming 进行自定义。

plugins

打包期间要使用的插件列表。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  plugins: [/* ... */],
})
CLI
n/a

Bun 为 Bun 的运行时和打包器实现了一个通用的插件系统。有关完整文档,请参阅 插件文档

sourcemap

指定要生成的源映射类型。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  sourcemap: "external", // default "none"
})
CLI
bun build ./index.tsx --outdir ./out --sourcemap=external
"none"默认。不生成源映射。

"inline"

生成源映射并作为 base64 有效负载附加到生成的包的末尾。

// <bundled code here>

//# sourceMappingURL=data:application/json;base64,<encoded sourcemap here>
"external"在每个 *.js 包旁边创建一个单独的 *.js.map 文件。

生成的包包含一个 调试 ID,该 ID 可用于将包与其对应的源映射关联起来。此 debugId 作为注释添加到文件底部。

// <generated bundle code>

//# debugId=<DEBUG ID>

关联的 *.js.map 源映射将是一个 JSON 文件,其中包含一个等效的 debugId 属性。

minify

是否启用缩小。默认 false

在针对 bun 时,默认情况下将缩小标识符。

要启用所有缩小选项

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  minify: true, // default false
})
CLI
bun build ./index.tsx --outdir ./out --minify

要精细地启用某些缩小

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  minify: {
    whitespace: true,
    identifiers: true,
    syntax: true,
  },
})
CLI
bun build ./index.tsx --outdir ./out --minify-whitespace --minify-identifiers --minify-syntax

external

要视为外部的一系列导入路径。默认为 []

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  external: ["lodash", "react"], // default: []
})
CLI
bun build ./index.tsx --outdir ./out --external lodash --external react

外部导入不会包含在最终包中。相反,import 语句将保持原样,以便在运行时解析。

例如,考虑以下入口文件

index.tsx
import _ from "lodash";
import {z} from "zod";

const value = z.string().parse("Hello world!")
console.log(_.upperCase(value));

通常,打包 index.tsx 会生成一个包,其中包含 "zod" 包的整个源代码。如果我们希望将 import 语句保持原样,我们可以将其标记为外部

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  external: ['zod'],
})
CLI
bun build ./index.tsx --outdir ./out --external zod

生成的包将类似于以下内容

out/index.js
import {z} from "zod";

// ...
// the contents of the "lodash" package
// including the `_.upperCase` function

var value = z.string().parse("Hello world!")
console.log(_.upperCase(value));

要将所有导入标记为外部,请使用通配符 *

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  external: ['*'],
})
CLI
bun build ./index.tsx --outdir ./out --external '*'

naming

自定义生成的文件名。默认为 ./[dir]/[name].[ext]

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  naming: "[dir]/[name].[ext]", // default
})
CLI
bun build ./index.tsx --outdir ./out --entry-naming [dir]/[name].[ext]

默认情况下,生成包的名称基于关联入口点的名称。

.
├── index.tsx
└── out
    └── index.js

对于多个入口点,生成的文件层次结构将反映入口点的目录结构。

.
├── index.tsx
└── nested
    └── index.tsx
└── out
    ├── index.js
    └── nested
        └── index.js

可以使用 naming 字段自定义生成文件的名称和位置。此字段接受模板字符串,该字符串用于为所有与入口点对应的包生成文件名,其中以下标记将替换为其相应的值

  • [name] - 入口点文件的名称(不带扩展名)。
  • [ext] - 生成包的扩展名。
  • [hash] - 包内容的哈希值。
  • [dir] - 从构建根目录到文件父目录的相对路径。

例如

标记[name][ext][hash][dir]
./index.tsxindexjsa1b2c3d4""(空字符串)
./nested/entry.tsentryjsc3d4e5f6"nested"

我们可以组合这些标记来创建一个模板字符串。例如,要在生成包名称中包含哈希值

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  naming: 'files/[dir]/[name]-[hash].[ext]',
})
CLI
bun build ./index.tsx --outdir ./out --entry-naming [name]-[hash].[ext]

此构建将生成以下文件结构

.
├── index.tsx
└── out
    └── files
        └── index-a1b2c3d4.js

当为 naming 字段提供 string 时,它仅用于与入口点对应的包。 和复制资产的名称不受影响。使用 JavaScript API,可以为每种类型的生成文件指定单独的模板字符串。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  naming: {
    // default values
    entry: '[dir]/[name].[ext]',
    chunk: '[name]-[hash].[ext]',
    asset: '[name]-[hash].[ext]',
  },
})
CLI
bun build ./index.tsx --outdir ./out --entry-naming "[dir]/[name].[ext]" --chunk-naming "[name]-[hash].[ext]" --asset-naming "[name]-[hash].[ext]"

root

项目的根目录。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./pages/a.tsx', './pages/b.tsx'],
  outdir: './out',
  root: '.',
})
CLI
n/a

如果未指定,则计算为所有入口点文件的第一个公共祖先。考虑以下文件结构

.
└── pages
  └── index.tsx
  └── settings.tsx

我们可以在 pages 目录中构建这两个入口点

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./pages/index.tsx', './pages/settings.tsx'],
  outdir: './out',
})
CLI
bun build ./pages/index.tsx ./pages/settings.tsx --outdir ./out

这将导致如下文件结构

.
└── pages
  └── index.tsx
  └── settings.tsx
└── out
  └── index.js
  └── settings.js

由于 pages 目录是入口点文件的第一个公共祖先,因此它被视为项目根目录。这意味着生成的包位于 out 目录的顶层;没有 out/pages 目录。

可以通过指定 root 选项来覆盖此行为

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./pages/index.tsx', './pages/settings.tsx'],
  outdir: './out',
  root: '.',
})
CLI
bun build ./pages/index.tsx ./pages/settings.tsx --outdir ./out --root .

通过将 . 指定为 root,生成的目录结构将如下所示

.
└── pages
  └── index.tsx
  └── settings.tsx
└── out
  └── pages
    └── index.js
    └── settings.js

publicPath

要附加到已捆绑代码中的任何导入路径的前缀。

在许多情况下,生成的包不会包含任何 import 语句。毕竟,捆绑的目标是将所有代码组合到一个文件中。但是,有许多情况下,生成的包将包含 import 语句。

  • 资产导入 — 当导入 *.svg 等无法识别的文件类型时,打包器会推迟到 file 加载器,该加载器会按原样将文件复制到 outdir 中。导入被转换为变量
  • 外部模块 — 文件和模块可以标记为 external,在这种情况下,它们不会包含在包中。相反,import 语句将保留在最终包中。
  • 块化。当 splitting 启用时,打包器可能会生成单独的“块”文件,这些文件表示在多个入口点之间共享的代码。

在任何这些情况下,最终包都可能包含到其他文件的路径。默认情况下,这些导入是相对的。以下是一个简单的资产导入示例

输入
输出
输入
import logo from './logo.svg';
console.log(logo);
输出
// logo.svg is copied into <outdir>
// and hash is added to the filename to prevent collisions
var logo = './logo-a7305bdef.svg';
console.log(logo);

设置 publicPath 将使用指定的值为所有文件路径添加前缀。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  publicPath: 'https://cdn.example.com/', // default is undefined
})
CLI
bun build ./index.tsx --outdir ./out --public-path https://cdn.example.com/

输出文件现在看起来像这样。

输出
var logo = './logo-a7305bdef.svg';
var logo = 'https://cdn.example.com/logo-a7305bdef.svg';

define

一个全局标识符映射,将在构建时替换。此对象的键是标识符名称,值是将被内联的 JSON 字符串。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  define: {
    STRING: JSON.stringify("value"),
    "nested.boolean": "true",
  },
})
CLI
bun build ./index.tsx --outdir ./out --define 'STRING="value"' --define "nested.boolean=true"

loader

文件扩展名到 内置加载器名称 的映射。这可用于快速定制某些文件的加载方式。

JavaScript
CLI
JavaScript
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: './out',
  loader: {
    ".png": "dataurl",
    ".txt": "file",
  },
})
CLI
bun build ./index.tsx --outdir ./out --loader .png:dataurl --loader .txt:file

输出

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) {
  await output.arrayBuffer(); // => ArrayBuffer
  await output.text(); // string
}

每个工件还包含以下属性

kind此文件是哪种构建输出。构建会生成捆绑的入口点、代码拆分“块”、源映射和复制的资产(如图像)。
path磁盘上文件的绝对路径
loader用于解释该文件的加载器。请参阅 Bundler > Loaders,了解 Bun 如何将文件扩展名映射到相应的内置加载器。
hash文件内容的哈希值。始终为资产定义。
sourcemap此文件对应的源映射文件(如果已生成)。仅为入口点和块定义。

BunFile 类似,BuildArtifact 对象可以直接传递到 new Response() 中。

const build = await Bun.build({
  /* */
});

const artifact = build.outputs[0];

// Content-Type header is automatically set
return new Response(artifact);

Bun 运行时实现了 BuildArtifact 对象的特殊美化打印,以便于调试。

构建脚本
Shell 输出
构建脚本
// build.ts
const build = await 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 支持将 JavaScript/TypeScript 入口点“编译”为独立的可执行文件。此可执行文件包含 Bun 二进制文件的一个副本。

bun build ./cli.tsx --outfile mycli --compile
./mycli

有关完整文档,请参阅 Bundler > Executables

日志和错误

仅当提供无效选项时,Bun.build 才会引发异常。读取 success 属性以确定构建是否成功;logs 属性将包含其他详细信息。

const result = await Bun.build({
  entrypoints: ["./index.tsx"],
  outdir: "./out",
});

if (!result.success) {
  console.error("Build failed");
  for (const message of result.logs) {
    // Bun will pretty print the message object
    console.error(message);
  }
}

每条消息都是一个 BuildMessageResolveMessage 对象,可用于追踪构建中发生的问题。

class BuildMessage {
  name: string;
  position?: Position;
  message: string;
  level: "error" | "warning" | "info" | "debug" | "verbose";
}

class ResolveMessage extends BuildMessage {
  code: string;
  referrer: string;
  specifier: string;
  importKind: ImportKind;
}

如果您想从失败的构建中引发错误,请考虑将日志传递给 AggregateError。如果未捕获,Bun 会漂亮地打印包含的消息。

if (!result.success) {
  throw new AggregateError(result.logs, "Build failed");
}

参考

interface Bun {
  build(options: BuildOptions): Promise<BuildOutput>;
}

interface BuildOptions {
  entrypoints: string[]; // required
  outdir?: string; // default: no write (in-memory only)
  format?: "esm"; // later: "cjs" | "iife"
  target?: "browser" | "bun" | "node"; // "browser"
  splitting?: boolean; // true
  plugins?: BunPlugin[]; // [] // See https://bun.net.cn/docs/bundler/plugins
  loader?: { [k in string]: Loader }; // See https://bun.net.cn/docs/bundler/loaders
  manifest?: boolean; // false
  external?: string[]; // []
  sourcemap?: "none" | "inline" | "external"; // "none"
  root?: string; // computed from entrypoints
  naming?:
    | string
    | {
        entry?: string; // '[dir]/[name].[ext]'
        chunk?: string; // '[name]-[hash].[ext]'
        asset?: string; // '[name]-[hash].[ext]'
      };
  publicPath?: string; // e.g. http://mydomain.com/
  minify?:
    | boolean // false
    | {
        identifiers?: boolean;
        whitespace?: boolean;
        syntax?: boolean;
      };
}

interface BuildOutput {
  outputs: BuildArtifact[];
  success: boolean;
  logs: Array<BuildMessage | ResolveMessage>;
}

interface BuildArtifact extends Blob {
  path: string;
  loader: Loader;
  hash?: string;
  kind: "entry-point" | "chunk" | "asset" | "sourcemap";
  sourcemap?: BuildArtifact;
}

type Loader =
  | "js"
  | "jsx"
  | "ts"
  | "tsx"
  | "json"
  | "toml"
  | "file"
  | "napi"
  | "wasm"
  | "text";

interface BuildOutput {
  outputs: BuildArtifact[];
  success: boolean;
  logs: Array<BuildMessage | ResolveMessage>;
}

declare class ResolveMessage {
  readonly name: "ResolveMessage";
  readonly position: Position | null;
  readonly code: string;
  readonly message: string;
  readonly referrer: string;
  readonly specifier: string;
  readonly importKind:
    | "entry_point"
    | "stmt"
    | "require"
    | "import"
    | "dynamic"
    | "require_resolve"
    | "at"
    | "at_conditional"
    | "url"
    | "internal";
  readonly level: "error" | "warning" | "info" | "debug" | "verbose";

  toString(): string;
}