
如果你曾經寫過一段 build script,專門用來讀取 package.json 版本號、抓取 Git commit hash,
或是把某個 JSON 檔案內嵌進最終產出的 bundle 裡——你可能已經體會過「build-time 邏輯」跟「runtime 邏輯」被硬生生拆成兩套系統的痛
webpack 有 DefinePlugin,esbuild 有 define,
但這些工具通常只能處理簡單的字串替換,遇到稍微複雜一點的邏輯,還是得另外寫一支獨立的 script。
Bun 給了另一種解法:Macros
它讓你直接用 JavaScript/TypeScript 寫一個「在打包階段執行」的函式,
執行結果會被原封不動地內嵌進最終的 bundle 裡——函式本身的原始碼則完全不會出現在輸出中
這篇文章就來聊聊這個機制到底怎麼運作、能用來做什麼,以及使用上要注意哪些陷阱
我們這裡用官方提供的一個例子,作為練習的標準
我們先建立一個簡單的 random function 並且建立這個檔案 random.ts
export function random() {
return Math.random();
}
這就只是一個很普通的函式,一個很普通的檔案,但只要用 with { type 'marco' }
這個 import attribute 把它引入,Bun 就會啟動這個黑魔法把它當成 marco 對待
我們這裡依照官方的檔案命名,叫做 cli.tsx 做使用
import { random } from "./random.ts" with { type: 'macro' };
console.log(`這個是 random number : ${random()}`)
補充 : 雖然官方用 tsx 但未必是要用 tsx 一般的 ts 也是有用的
怕有人誤解這一段,所以補充一下
我們這看看結果
這代表在 build-time 的時候就拿到 random number 的結果
這代表他達到
編譯時求值,執行時只拿到結果
如果有不懂意思,可以把 with { type: 'macro' } 拿掉執行看看就清楚了
bun 的 Transpiler 在解析程式碼時,如果看到 macro import
會用 bun 自己的 Javascript runtime 先去呼叫這個函式
並把回傳值回傳為 AST 的節點
這件事情發生很早,macro 是在 transpiler 的 visiting phase 中同步執行的
早於 plugins 也早於 transpiler 的 AST
它們會依照被 import 的順序依序執行,
transpiler 會等每個 macro 執行完才繼續,
如果 macro 回傳 Promise 也會被 await
可以參考以下流程看整個生命週期
注意 :
這值得一提一件事,Bun 的 bundler 是多執行緒,所以多個 macro
會平行啟動 javascript worker 中並行執行
優點是 : 代表假設有一堆macro 也不太會拖慢打包時間
值得住一的是 : macro 只有真的被打包時才會生效
舉個例子 :
bun build src/index.ts --outdir dist # 這個 macro 會執行
bun run src/index.ts # 這個 macro 不會執行
如果有注意上面流程圖會發現,bundler 會在 macro 內嵌完成後才進行 DCE
那這時候就引出一間值得思考的事情 ? 什麼時候可以被內嵌
白話文就是
所有 JSON都完全支援
像是這樣
export function getSample() {
return {
name: 'hello',
foo: 'cool',
}
}
但以下例子會炸開
sample.ts
export function returnFalseFn() {
return false;
}
index.ts
import { returnFalseFn } from './sample' with { type: 'macro' }
if (returnFalseFn()) {
console.log('這會被 DCE 砍掉');
}
以這種狀況,底下的 code build 等同於不存在
sample.ts
export function returnEmptyCurryFn() {
return () => {}
}
index.ts
import { returnEmptyCurryFn } from './sample' with { type: 'macro' }
// 這個也會炸開
看到這裡,你應該已經抓到 Bun Macros 的核心概念了:
它不是語法糖,而是一個貨真價實的「編譯時期程式碼執行環境」
透過 with { type: 'macro' },我們讓 Transpiler 在 visiting phase 就同步(甚至平行)執行這些函式,
並把回傳值直接烙印進最終的 AST 裡——原始碼消失,只留下結果。這代表:
build-time 邏輯不再需要另外寫一支獨立 script
也不用依賴 webpack DefinePlugin 或 esbuild define 那種只能處理字串替換的陽春工具
更不用在 CI 裡多跑一道「產生設定檔」的步驟
但這篇只講了基礎機制跟序列化的限制——能回傳什麼、不能回傳什麼。真正好玩的東西還在後面。
接下來就是 Bun macro 的進階實戰篇了