巨集是一種在打包時執行 JavaScript 函數的機制。從這些函數返回的值會直接內聯到您的 bundle 中。
作為一個簡單的範例,請考慮這個返回隨機數的簡單函數。
export function random() {
return Math.random();
}
這只是一個常規檔案中的常規函數,但我們可以像這樣將其用作巨集
import { random } from './random.ts' with { type: 'macro' };
console.log(`Your random number is ${random()}`);
注意 — 巨集使用匯入屬性語法表示。如果您以前沒有見過這種語法,它是一個 Stage 3 TC39 提案,允許您將額外的元數據附加到 import
語句。
現在我們將使用 bun build
打包這個檔案。打包後的檔案將列印到標準輸出。
bun build ./cli.tsx
console.log(`Your random number is ${0.6805550949689833}`);
如您所見,random
函數的原始碼在 bundle 中任何地方都沒有出現。相反,它是在打包期間執行的,並且函數呼叫 (random()
) 被替換為函數的結果。由於原始碼永遠不會包含在 bundle 中,因此巨集可以安全地執行特權操作,例如從資料庫讀取。
何時使用巨集
如果您有幾個用於小型任務的建置腳本,在這些情況下您可能需要一次性建置腳本,則 bundle-time 程式碼執行可能更易於維護。它與您的其餘程式碼一起存在,與其餘建置一起執行,自動並行化,並且如果失敗,建置也會失敗。
但是,如果您發現自己在 bundle-time 執行大量程式碼,請考慮改為執行伺服器。
匯入屬性
Bun 巨集是使用以下任一項註解的 import 語句
with { type: 'macro' }
— 一個 匯入屬性,Stage 3 ECMA Scrdassert { type: 'macro' }
— 一個 import assertion,是匯入屬性的早期版本,現在已被放棄(但 已經被許多瀏覽器和執行時環境支援)
安全考量
巨集必須使用 { type: "macro" }
明確匯入才能在 bundle-time 執行。這些匯入如果未被呼叫則無效,這與可能具有副作用的常規 JavaScript 匯入不同。
您可以通過將 --no-macros
標誌傳遞給 Bun 來完全禁用巨集。它會產生如下建置錯誤
error: Macros are disabled
foo();
^
./hello.js:3:1 53
為了減少惡意套件的潛在攻擊面,巨集無法從 node_modules/**/*
內部調用。如果套件嘗試調用巨集,您會看到如下錯誤
error: For security reasons, macros cannot be run from node_modules.
beEvil();
^
node_modules/evil/index.js:3:1 50
您的應用程式碼仍然可以從 node_modules
匯入巨集並調用它們。
import {macro} from "some-package" with { type: "macro" };
macro();
匯出條件 "macro"
當將包含巨集的函式庫發佈到 npm
或另一個套件註冊表時,請使用 "macro"
匯出條件,以專門為巨集環境提供特殊版本的套件。
{
"name": "my-package",
"exports": {
"import": "./index.js",
"require": "./index.js",
"default": "./index.js",
"macro": "./index.macro.js"
}
}
通過此配置,用戶可以在執行時或在 bundle-time 使用相同的 import specifier 來使用您的套件
import pkg from "my-package"; // runtime import
import {macro} from "my-package" with { type: "macro" }; // macro import
第一個 import 將解析為 ./node_modules/my-package/index.js
,而第二個將由 Bun 的 bundler 解析為 ./node_modules/my-package/index.macro.js
。
執行
當 Bun 的 transpiler 看到巨集匯入時,它會使用 Bun 的 JavaScript 執行時環境在 transpiler 內部呼叫該函數,並將 JavaScript 的返回值轉換為 AST 節點。這些 JavaScript 函數在 bundle-time 而不是執行時呼叫。
巨集在訪問階段(在外掛程式之前和 transpiler 生成 AST 之前)在 transpiler 中同步執行。它們按照匯入的順序執行。transpiler 將等待巨集完成執行後再繼續。transpiler 也將 await
巨集返回的任何 Promise
。
Bun 的 bundler 是多執行緒的。因此,巨集在多個產生的 JavaScript "workers" 內部並行執行。
無效程式碼消除
bundler 在執行和內聯巨集之後執行無效程式碼消除。因此,給定以下巨集
export function returnFalse() {
return false;
}
...然後打包以下檔案將產生一個空的 bundle,前提是啟用 minify syntax 選項。
import {returnFalse} from './returnFalse.ts' with { type: 'macro' };
if (returnFalse()) {
console.log("This code is eliminated");
}
可序列化
Bun 的 transpiler 需要能夠序列化巨集的結果,以便可以將其內聯到 AST 中。支援所有 JSON 兼容的資料結構
export function getObject() {
return {
foo: "bar",
baz: 123,
array: [ 1, 2, { nested: "value" }],
};
}
巨集可以是異步的,或者返回 Promise
實例。Bun 的 transpiler 將自動 await
Promise
並內聯結果。
export async function getText() {
return "async value";
}
transpiler 實現了特殊的邏輯來序列化常見的資料格式,例如 Response
、Blob
、TypedArray
。
TypedArray
:解析為 base64 編碼的字串。Response
:Bun 將讀取Content-Type
並相應地進行序列化;例如,類型為application/json
的Response
將自動解析為物件,而text/plain
將作為字串內聯。具有無法識別或undefined
type
的 Response 將以 base-64 編碼。Blob
:與Response
一樣,序列化取決於type
屬性。
fetch
的結果是 Promise<Response>
,因此可以直接返回。
export function getObject() {
return fetch("https://bun.dev.org.tw")
}
函數和大多數類別的實例(上面提到的那些除外)是不可序列化的。
export function getText(url: string) {
// this doesn't work!
return () => {};
}
參數
巨集可以接受輸入,但僅在有限的情況下。該值必須是靜態已知的。例如,以下是不允許的
import {getText} from './getText.ts' with { type: 'macro' };
export function howLong() {
// the value of `foo` cannot be statically known
const foo = Math.random() ? "foo" : "bar";
const text = getText(`https://example.com/${foo}`);
console.log("The page is ", text.length, " characters long");
}
但是,如果在 bundle-time 已知 foo
的值(例如,如果它是常數或另一個巨集的結果),則它是允許的
import {getText} from './getText.ts' with { type: 'macro' };
import {getFoo} from './getFoo.ts' with { type: 'macro' };
export function howLong() {
// this works because getFoo() is statically known
const foo = getFoo();
const text = getText(`https://example.com/${foo}`);
console.log("The page is", text.length, "characters long");
}
這輸出
function howLong() {
console.log("The page is", 1322, "characters long");
}
export { howLong };
範例
嵌入最新的 git commit 雜湊值
export function getGitCommitHash() {
const {stdout} = Bun.spawnSync({
cmd: ["git", "rev-parse", "HEAD"],
stdout: "pipe",
});
return stdout.toString();
}
當我們建置它時,getGitCommitHash
會被替換為呼叫函數的結果
import { getGitCommitHash } from './getGitCommitHash.ts' with { type: 'macro' };
console.log(`The current Git commit hash is ${getGitCommitHash()}`);
console.log(`The current Git commit hash is 3ee3259104f`);
您可能在想「為什麼不直接使用 process.env.GIT_COMMIT_HASH
?」 嗯,您也可以這樣做。但是您可以使用環境變數來做到這一點嗎?
在 bundle-time 發出 fetch()
請求
在本範例中,我們使用 fetch()
發出外發 HTTP 請求,使用 HTMLRewriter
解析 HTML 回應,並返回一個包含標題和 meta 標籤的物件——所有這些都在 bundle-time 完成。
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;
}
extractMetaTags
函數在 bundle-time 被擦除,並替換為函數呼叫的結果。這意味著 fetch
請求在 bundle-time 發生,並且結果嵌入在 bundle 中。此外,拋出錯誤的分支被消除,因為它是無法到達的。
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("Expected title to be 'Example Domain'");
}
return <head>
<title>{headTags.title}</title>
<meta name="viewport" content={headTags.viewport} />
</head>;
};
import { jsx, jsxs } from "react/jsx-runtime";
export const Head = () => {
jsxs("head", {
children: [
jsx("title", {
children: "Example Domain",
}),
jsx("meta", {
name: "viewport",
content: "width=device-width, initial-scale=1",
}),
],
});
};
export { Head };