Bun 的快速原生打包器現在處於測試階段。它可以使用 bun build
CLI 命令或 Bun.build()
JavaScript API。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './build',
});
bun build ./index.tsx --outdir ./build
速度很快。以下數字代表 esbuild 在 three.js 基準 上的效能。

為何要打包?
打包器是 JavaScript 生態系統中的重要基礎架構。以下是打包如此重要的簡要說明
- 減少 HTTP 要求。
node_modules
中的單一套件可能包含數百個檔案,而大型應用程式可能會有數十個此類依賴項。使用個別 HTTP 要求載入這些檔案很快就會變得難以處理,因此打包器用於將我們的應用程式原始碼轉換成較少數量的自給自足「套件」,這些套件可以使用單一要求載入。 - 程式碼轉換。現代應用程式通常使用 TypeScript、JSX 和 CSS 模組等語言或工具建置,所有這些都必須轉換成純 JavaScript 和 CSS,才能讓瀏覽器使用。打包器是設定這些轉換的自然所在。
- 架構功能。架構仰賴打包器外掛程式和程式碼轉換來實作常見模式,例如檔案系統路由、客戶端伺服器程式碼共置(想想
getServerSideProps
或 Remix 載入器)和伺服器元件。
讓我們深入了解打包器 API。
請注意,Bun 打包器並非用於取代 tsc
進行類型檢查或產生類型宣告。
基本範例
讓我們建置第一個套件。您有以下兩個檔案,它們實作一個簡單的客戶端渲染 React 應用程式。
import * as ReactDOM from 'react-dom/client';
import {Component} from "./Component"
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<Component message="Sup!" />)
export function Component(props: {message: string}) {
return <p>{props.message}</p>
}
在此,index.tsx
是我們應用程式的「進入點」。通常,這會是一個執行某些副作用的指令碼,例如啟動伺服器或(在本例中)初始化 React 根。由於我們使用 TypeScript 和 JSX,因此需要在將程式碼傳送至瀏覽器之前先打包我們的程式碼。
建立我們的套件
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
})
bun build ./index.tsx --outdir ./out
對於 entrypoints
中指定的每個檔案,Bun 會產生一個新的套件。此套件會寫入到磁碟中的 ./out
目錄(從目前的作業目錄解析)。執行建置後,檔案系統會如下所示
.
├── index.tsx
├── Component.tsx
└── 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 檔案會被解析並內嵌到套件中,作為 JavaScript 物件。
|
| TOML 檔案會被解析並內嵌到套件中,作為 JavaScript 物件。
|
| 文字檔的內容會讀取並以字串形式內嵌到套件中。
|
.node .wasm | Bun 執行階段支援這些檔案,但在套件處理期間,它們會被視為 資產。 |
資產
如果套件處理器遇到無法辨識的副檔名匯入,它會將匯入的檔案視為外部檔案。所引用的檔案會原樣複製到 outdir
,而匯入會解析為檔案的路徑。
// bundle entrypoint
import logo from "./logo.svg";
console.log(logo);
// bundled output
var logo = "./logo-ab237dfe.svg";
console.log(logo);
檔案載入器的確切行為也會受到 命名
和 publicPath
的影響。
請參閱 套件處理器 > 載入器 頁面,以取得檔案載入器的更完整文件。
外掛程式
此表格中所述的行為可以用 外掛程式 覆寫或擴充。請參閱 套件處理器 > 載入器 頁面,以取得完整的說明文件。
API
entrypoints
必要。一個路徑陣列,對應到我們應用程式的進入點。會為每個進入點產生一個套件。
const result = await Bun.build({
entrypoints: ["./index.ts"],
});
// => { success: boolean, outputs: BuildArtifact[], logs: BuildMessage[] }
bun build --entrypoints ./index.ts
# the bundle will be printed to stdout
# <bundled code>
outdir
輸出檔案會寫入的目錄。
const result = await Bun.build({
entrypoints: ['./index.ts'],
outdir: './out'
});
// => { success: boolean, outputs: BuildArtifact[], logs: BuildMessage[] }
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
套件預計執行的環境。
await Bun.build({
entrypoints: ['./index.ts'],
outdir: './out',
target: 'browser', // default
})
bun build --entrypoints ./index.ts --outdir ./out --target browser
Bun 會根據目標套用不同的模組解析規則和最佳化。
browser | 預設。用於產生預計由瀏覽器執行的套件。解析匯入時,優先考慮 "browser" 匯出條件。匯入任何內建模組,例如 node:events 或 node:path 都會運作,但呼叫某些函式,例如 fs.readFile 則無法運作。 |
| 用於產生預計由 Bun 執行時期執行的套件。在許多情況下,不需要套件伺服器端程式碼;你可以直接執行原始碼而不用修改。然而,套件伺服器端程式碼可以減少啟動時間並提升執行效能。 所有以 如果任何進入點包含 Bun shebang ( |
node | 用於產生預計由 Node.js 執行的套件。解析匯入時,優先考慮 "node" 匯出條件,並輸出 .mjs 。未來,這將會自動填充 Bun 全域變數和其他內建 bun:* 模組,儘管這尚未實作。 |
format
指定在產生的套件中使用的模組格式。
目前套件產生器只支援一種模組格式:"esm"
。計畫支援 "cjs"
和 "iife"
。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
format: "esm",
})
bun build ./index.tsx --outdir ./out --format esm
splitting
是否啟用程式碼拆分。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
splitting: false, // default
})
bun build ./index.tsx --outdir ./out --splitting
當 true
時,bundler 將啟用程式碼拆分。當多個進入點同時匯入同一個檔案、模組或檔案/模組組時,通常會將共用程式碼拆分到一個獨立的 bundle。這個共用 bundle 稱為區塊。考慮以下檔案
import { shared } from './shared.ts';
import { shared } from './shared.ts';
export const shared = 'shared';
若要使用啟用程式碼拆分的 entry-a.ts
和 entry-b.ts
await Bun.build({
entrypoints: ['./entry-a.ts', './entry-b.ts'],
outdir: './out',
splitting: true,
})
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
在 bundling 期間使用的外掛清單。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
plugins: [/* ... */],
})
n/a
Bun 為 Bun 的執行時間和 bundler 實作一個通用外掛系統。請參閱 外掛文件 以取得完整文件。
sourcemap
指定要產生的 sourcemap 類型。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
sourcemap: "external", // default "none"
})
bun build ./index.tsx --outdir ./out --sourcemap=external
"none" | 預設。不會產生 sourcemap。 |
| 會產生 sourcemap,並附加到產生的 bundle 的結尾,作為 base64 payload。
|
"external" | 會在每個 *.js bundle 旁邊建立一個獨立的 *.js.map 檔案。 |
產生的 bundle 包含一個 偵錯 ID,可用於將 bundle 與其對應的 sourcemap 關聯起來。這個 debugId
會新增為檔案底部的註解。
// <generated bundle code>
//# debugId=<DEBUG ID>
關聯的 *.js.map
sourcemap 會是一個 JSON 檔案,包含一個等效的 debugId
屬性。
minify
是否啟用縮小化。預設為 false
。
當目標為 bun
時,預設會縮小化識別碼。
若要啟用所有縮小化選項
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
minify: true, // default false
})
bun build ./index.tsx --outdir ./out --minify
若要詳細啟用某些縮小化
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
minify: {
whitespace: true,
identifiers: true,
syntax: true,
},
})
bun build ./index.tsx --outdir ./out --minify-whitespace --minify-identifiers --minify-syntax
external
一個要考慮為外部的匯入路徑清單。預設為 []
。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
external: ["lodash", "react"], // default: []
})
bun build ./index.tsx --outdir ./out --external lodash --external react
外部匯入是不會包含在最終套件中的。相反地,import
陳述會保持原樣,在執行階段解析。
例如,考慮下列入口點檔案
import _ from "lodash";
import {z} from "zod";
const value = z.string().parse("Hello world!")
console.log(_.upperCase(value));
通常,套件化 index.tsx
會產生一個套件,其中包含 "zod"
套件的完整原始碼。如果我們希望保持 import
陳述原樣,我們可以將其標記為外部
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
external: ['zod'],
})
bun build ./index.tsx --outdir ./out --external zod
產生的套件會類似這樣
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));
若要將所有匯入標記為外部,請使用萬用字元 *
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
external: ['*'],
})
bun build ./index.tsx --outdir ./out --external '*'
naming
自訂產生的檔案名稱。預設為 ./[dir]/[name].[ext]
。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
naming: "[dir]/[name].[ext]", // default
})
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.tsx | index | js | a1b2c3d4 | "" (空字串) |
./nested/entry.ts | entry | js | c3d4e5f6 | "nested" |
我們可以組合這些代碼來建立一個範本字串。例如,要在產生的套件名稱中包含雜湊
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
naming: 'files/[dir]/[name]-[hash].[ext]',
})
bun build ./index.tsx --outdir ./out --entry-naming [name]-[hash].[ext]
此建置會產生下列檔案結構
.
├── index.tsx
└── out
└── files
└── index-a1b2c3d4.js
當為 naming
欄位提供 string
時,它只會用於對應於入口點的套件。不會影響區塊和複製資產的名稱。使用 JavaScript API,可以為每種類型的產生的檔案指定不同的範本字串。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
naming: {
// default values
entry: '[dir]/[name].[ext]',
chunk: '[name]-[hash].[ext]',
asset: '[name]-[hash].[ext]',
},
})
bun build ./index.tsx --outdir ./out --entry-naming "[dir]/[name].[ext]" --chunk-naming "[name]-[hash].[ext]" --asset-naming "[name]-[hash].[ext]"
root
專案的根目錄。
await Bun.build({
entrypoints: ['./pages/a.tsx', './pages/b.tsx'],
outdir: './out',
root: '.',
})
n/a
如果未指定,會計算為所有入口點檔案的第一個共同祖先。考慮下列檔案結構
.
└── pages
└── index.tsx
└── settings.tsx
我們可以在 pages
目錄中建置兩個入口點
await Bun.build({
entrypoints: ['./pages/index.tsx', './pages/settings.tsx'],
outdir: './out',
})
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
選項來覆寫此行為
await Bun.build({
entrypoints: ['./pages/index.tsx', './pages/settings.tsx'],
outdir: './out',
root: '.',
})
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
會使用指定的值為所有檔案路徑加上前置詞。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
publicPath: 'https://cdn.example.com/', // default is undefined
})
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 字串。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
define: {
STRING: JSON.stringify("value"),
"nested.boolean": "true",
},
})
bun build ./index.tsx --outdir ./out --define 'STRING="value"' --define "nested.boolean=true"
loader
檔案副檔名對應到內建載入器名稱的對應表。這可以用來快速自訂如何載入特定檔案。
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
loader: {
".png": "dataurl",
".txt": "file",
},
})
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
物件的特殊漂亮列印,以簡化除錯。
// build.ts
const build = await Bun.build({/* */});
const artifact = build.outputs[0];
console.log(artifact);
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);
}
}
每則訊息都是 BuildMessage
或 ResolveMessage
物件,可用於追蹤建置中發生的問題。
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.dev.org.tw/docs/bundler/plugins
loader?: { [k in string]: Loader }; // See https://bun.dev.org.tw/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;
}