Bun

Bun Bundler


Jarred Sumner · 2023 年 5 月 16 日

Bun 快速的原生捆綁器現已推出 Beta 版。它可透過 bun build CLI 命令或新的 Bun.build() JavaScript API 使用。

從頭開始捆綁 10 份 three.js,包含原始碼對應 (sourcemaps) 和最小化 (minification)

使用捆綁器透過內建的 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?你該如何載入僅限本機的設定?我是否需要安裝同層級依賴項 (peer dependencies)?我該如何讓原始碼對應 (sourcemaps) 正常運作?

複雜性會耗費時間,通常花在將工具黏合在一起或等待事物完成。安裝 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,可與捆綁器運行時環境搭配使用。任何擴展 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.dev.org.tw/docs/bundler/plugins
  loader?: { [k in string]: string }; // see https://bun.dev.org.tw/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 會自動轉譯為原生 JavaScript。
  • 模組在可用時會使用 "browser" package.json "exports" 條件來解析
  • 當在瀏覽器中匯入某些 Node.js API(例如 node:crypto)時,Bun 會自動進行 polyfill,類似於 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此檔案的建置輸出類型。建置會產生捆綁的進入點、程式碼分割「區塊」、原始碼對應 (sourcemaps) 和複製的資產(例如圖片)。
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 Module 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,並對未使用的 exports 物件執行 tree-shaking。

已捆綁
// 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 不會對 exports 物件執行 tree-shaking;相反,它會注入一些 CommonJS 運行時環境程式碼,使其按預期運作。

CommonJS 包裝器

原始碼對應 (Source maps)

捆綁器同時支援內嵌和外部原始碼對應 (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 捆綁器就不完整。此版本也推出了 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 實作原始碼對應 (source maps) 支援並修復了許多錯誤。