執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

宏

使用 Bun 宏在打包時執行 JavaScript 函式

宏是打包時執行的 JavaScript 函式。Bun 會將它們的傳回值直接內聯到你的 bundle 中。

作為一個簡單範例,考慮下面這個傳回隨機數的函式。

random.ts
export function random() {
  return Math.random();
}

這是一個普通檔案中的普通函式,但你可以將它用作宏:

cli.tsx
import { random } from "./random.ts" with { type: "macro" };

console.log(`你的隨機數是 ${random()}`);

使用 bun build 打包檔案。Bun 會將打包後的檔案列印到 stdout。

terminal
bun build ./cli.tsx
JavaScript
console.log(`你的隨機數是 ${0.6805550949689833}`);

random 函式的原始碼不會出現在 bundle 中。相反,該函式會在打包期間執行,Bun 會將呼叫(random())替換為其結果。由於原始碼不會包含在 bundle 中,宏可以安全地執行讀取資料庫等特權操作。

何時使用宏

對於一些原本需要編寫一次性建置指令碼才能完成的小任務,在打包時執行程式碼會更易於維護。它與其餘程式碼放在一起,也會隨其餘建置流程一起執行。Bun 會自動並行執行宏;如果宏執行失敗,建置也會失敗。

不過,如果你發現自己在打包時執行大量程式碼,考慮改為執行伺服器。

匯入屬性

宏是帶有以下註解之一的匯入語句:

  • with { type: 'macro' } — 匯入屬性,是一項 Stage 4 ECMAScript 提案
  • assert { type: 'macro' } — 匯入斷言,是匯入屬性的早期形式,現已被棄用(但一些瀏覽器和執行環境已經支援它)

安全注意事項

必須使用 { type: "macro" } 顯式匯入宏,宏才會在打包時執行。這些匯入只有在呼叫時才會生效,不同於可能產生副作用的普通 JavaScript 匯入。

你可以使用 --no-macros 標誌完全停用宏。它會產生如下建置錯誤:

Code
error: Macros are disabled

foo();
^
./hello.js:3:1 53

為減少惡意包可能造成的攻擊面,Bun 不允許 node_modules/**/* 中的程式碼呼叫宏。如果某個包嘗試呼叫宏,你會看到類似這樣的錯誤:

Code
error: For security reasons, macros cannot be run from node_modules.

beEvil();
^
node_modules/evil/index.js:3:1 50

你的應用程式碼仍然可以從 node_modules 引入宏並呼叫它們。

cli.tsx
import { macro } from "some-package" with { type: "macro" };

macro();

匯出條件 "macro"

當將包含宏的庫釋出到 npm 或其他包登錄檔時,使用 "macro" 匯出條件,為宏環境提供專用的包版本。

package.json
{
  "name": "my-package",
  "exports": {
    "import": "./index.js",
    "require": "./index.js",
    "default": "./index.js",
    "macro": "./index.macro.js"
  }
}

這個設定允許使用者使用相同的匯入識別符號在執行環境或打包時消費你的包:

index.ts
import pkg from "my-package"; // 執行環境匯入
import { macro } from "my-package" with { type: "macro" }; // 宏匯入

第一個匯入解析為 ./node_modules/my-package/index.js;Bun 的打包器會將第二個匯入解析為 ./node_modules/my-package/index.macro.js。

執行

當 Bun 的轉譯器看到宏匯入時,它會使用 Bun 的 JavaScript 執行環境呼叫該函式,並將傳回值轉換為 AST 節點。

宏會在轉譯器的訪問階段同步執行,此時轉譯器已經將檔案解析為 AST。它們按照呼叫在檔案中出現的順序執行;轉譯器只有在遇到對某個宏模組匯出的函式的呼叫時,才會載入或執行該宏模組。轉譯器會等待每個宏完成後再繼續,並會等待宏傳回的任何 Promise。

Bun 的打包器是多執行緒的,因此宏會在多個生成的 JavaScript“工作執行緒”中並行執行。

死程式碼消除

打包器會在執行並內聯宏之後執行死程式碼消除。給定以下宏:

returnFalse.ts
export function returnFalse() {
  return false;
}

……在啟用 minify 語法選項的情況下,打包以下檔案會生成一個空的 bundle。

index.ts
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };

if (returnFalse()) {
  console.log("這段程式碼會被消除");
}

可序列化性

Bun 的轉譯器必須能夠序列化宏的結果,以便將其內聯到 AST 中。所有與 JSON 相容的資料結構都受支援:

macro.ts
export function getObject() {
  return {
    foo: "bar",
    baz: 123,
    array: [1, 2, { nested: "value" }],
  };
}

宏可以是非同步的,也可以傳回 Promise 例項。Bun 的轉譯器會等待 Promise,並將結果內聯。

macro.ts
export async function getText() {
  return "非同步值";
}

轉譯器實現了特殊邏輯,用於序列化 Response 和 Blob 等常見資料格式。

  • Response:Bun 會讀取 Content-Type 並據此進行序列化。例如,它會將型別為 application/json 的 Response 解析為物件,並將 text/plain 內聯為字串。對於型別無法識別或未定義的 Response,Bun 會使用 base64 編碼。
  • Blob:與 Response 一樣,序列化方式取決於 type 屬性。

fetch 的結果是 Promise<Response>,因此宏可以直接傳回它。

macro.ts
export function getObject() {
  return fetch("https://bun.sh");
}

函式和大多數類的例項(之前列出的例項除外)都無法序列化。

macro.ts
export function getText(url: string) {
  // 這行不行!
  return () => {};
}

引數

宏可以接受輸入,但僅限於有限情況。引數值必須是靜態已知的。例如,以下用法不被允許:

index.ts
import { getText } from "./getText.ts" with { type: "macro" };

export function howLong() {
  // `foo` 的值不能被靜態獲知
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("頁面長度為 ", text.length, " 字元");
}

但是,如果 foo 的值在打包時已知(例如,它是一個常量或另一個宏的結果),那麼就允許這樣呼叫:

index.ts
import { getText } from "./getText.ts" with { type: "macro" };
import { getFoo } from "./getFoo.ts" with { type: "macro" };

export function howLong() {
  // 這裡有效,因為 getFoo() 是靜態已知的
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("頁面長度為", text.length, "字元");
}

其輸出為:

JavaScript
function howLong() {
  console.log("頁面長度為", 1322, "字元");
}
export { howLong };

範例

嵌入最新 git 提交雜湊

getGitCommitHash.ts
export function getGitCommitHash() {
  const { stdout } = Bun.spawnSync({
    cmd: ["git", "rev-parse", "HEAD"],
    stdout: "pipe",
  });

  return stdout.toString();
}

打包時,Bun 會將 getGitCommitHash 呼叫替換為呼叫該函式的結果:

input
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };

console.log(`當前 Git 提交雜湊是 ${getGitCommitHash()}`);
output
console.log(`當前 Git 提交雜湊是 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

在打包時執行 fetch() 請求

此範例使用 fetch() 發出 HTTP 請求,使用 HTMLRewriter 解析 HTML 響應,並在打包時傳回一個包含標題和 meta 標籤的物件。

meta.ts
export async function extractMetaTags(url: string) {
  const response = await fetch(url);
  const meta = {
    title: "",
  };
  new HTMLRewriter()
    .on("title", {
      text(element) {
        meta.title += element.text;
      },
    })
    .on("meta", {
      element(element) {
        const name =
          element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop");

        if (name) meta[name] = element.getAttribute("content");
      },
    })
    .transform(response);

  return meta;
}

Bun 會在打包時擦除 extractMetaTags 函式,並將其替換為函式呼叫的結果。fetch 請求會在打包時發生,Bun 會將結果嵌入 bundle 中。啟用 minify 語法選項後,由於丟擲錯誤的分支不可達,Bun 也會將其消除。

input
import { extractMetaTags } from "./meta.ts" with { type: "macro" };

export const Head = () => {
  const headTags = extractMetaTags("https://example.com");

  if (headTags.title !== "Example Domain") {
    throw new Error("預期標題應為 'Example Domain'");
  }

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};
output
export const Head = () => {
  const headTags = {
    title: "Example Domain",
    viewport: "width=device-width, initial-scale=1",
  };

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};