マクロは、バンドル時に JavaScript 関数を実行するためのメカニズムです。これらの関数から返された値は、バンドルに直接インライン化されます。
簡単な例として、乱数を返すこの単純な関数を考えてください。
export function random() {
return Math.random();
}これは通常のファイル内の通常の関数ですが、次のようにマクロとして使用できます:
import { random } from "./random.ts" with { type: "macro" };
console.log(`Your random number is ${random()}`);NOTE
マクロはインポート属性構文を使用して示されます。この構文を見たことがない場合、これは Stage 3 TC39 提案で、インポート文に追加のメタデータを添付できます。次に、bun build でこのファイルをバンドルします。バンドルされたファイルは stdout に出力されます。
bun build ./cli.tsxconsole.log(`Your random number is ${0.6805550949689833}`);ご覧のとおり、random 関数のソースコードはバンドルに含まれていません。代わりに、バンドル中に実行され、関数呼び出し(random())は関数の結果に置き換えられます。ソースコードはバンドルに含まれないため、マクロはデータベースからの読み取りなどの特権操作を安全に実行できます。
マクロの使用時期
小規模なことのために複数のビルドスクリプトがある場合、バンドル時のコード実行はメンテナンスが容易になります。これはコードの残りと一緒に存在し、ビルドの残りと一緒に実行され、自動的に並列化され、失敗するとビルドも失敗します。
ただし、バンドル時に多くのコードを実行していることに気づいた場合は、代わりにサーバーを実行することを検討してください。
インポート属性
Bun マクロは、次のいずれかを使用して注釈付けられたインポート文です:
with { type: 'macro' }— インポート属性、Stage 3 ECMA Script 提案assert { type: 'macro' }— インポートアサーション、インポート属性の以前のバージョンで、現在は廃止されています(ただし、多くのブラウザとランタイムですでにサポートされています)
セキュリティの考慮事項
マクロは、バンドル時に実行されるために with { type: "macro" } で明示的にインポートする必要があります。これらのインポートは、呼び出されない場合は何の効果もありません。これは、副作用を持つ可能性のある通常の JavaScript インポートとは異なります。
Bun に --no-macros フラグを渡すことで、マクロを完全に無効にできます。次のようなビルドエラーが発生します:
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"
}
}この構成により、ユーザーは同じインポート指定子を使用して、ランタイムまたはバンドル時にパッケージを使用できます:
import pkg from "my-package"; // ランタイムインポート
import { macro } from "my-package" with { type: "macro" }; // マクロインポート最初のインポートは ./node_modules/my-package/index.js に解決され、2 番目は Bun のバンドラーによって ./node_modules/my-package/index.macro.js に解決されます。
実行
Bun のトランスパイラーがマクロインポートを検出すると、トランスパイラー内で Bun の JavaScript ランタイムを使用して関数を呼び出し、戻り値を JavaScript から AST ノードに変換します。これらの JavaScript 関数は、ランタイム時ではなくバンドル時に呼び出されます。
マクロは、訪問フェーズ中にトランスパイラー内で同期的に実行されます—プラグインの前、トランスパイラーが AST を生成する前です。マクロはインポートされた順序で実行されます。トランスパイラーは、マクロが実行を完了するまで待機します。トランスパイラーは、マクロによって返された Promise も待機します。
Bun のバンドラーはマルチスレッドです。そのため、マクロは複数の生成された JavaScript "ワーカー" 内で並列に実行されます。
デッドコード削除
バンドラーは、マクロを実行してインライン化した後にデッドコード削除を実行します。したがって、次のマクロが与えられた場合:
export function returnFalse() {
return false;
}... 次のファイルをバンドルすると、ミニファイ構文オプションが有効になっている場合、空のバンドルが生成されます。
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };
if (returnFalse()) {
console.log("This code is eliminated");
}直列化可能性
Bun のトランスパイラーは、マクロの結果を直列化して AST にインライン化できる必要があります。すべての JSON 互換のデータ構造がサポートされています:
export function getObject() {
return {
foo: "bar",
baz: 123,
array: [1, 2, { nested: "value" }],
};
}マクロは非同期にすることも、Promise インスタンスを返すこともできます。Bun のトランスパイラーは自動的に Promise を待機して結果をインライン化します。
export async function getText() {
return "async value";
}トランスパイラーは、Response、Blob、TypedArray などの一般的なデータ形式を直列化するための特別なロジックを実装しています。
- TypedArray: base64 エンコードされた文字列に解決されます。
- Response: Bun は
Content-Typeを読み取り、それに応じて直列化します。たとえば、タイプapplication/jsonの Response は自動的にオブジェクトに解析され、text/plainは文字列としてインライン化されます。認識されないまたは未定義のタイプの Response は base-64 エンコードされます。 - Blob: Response と同様に、
typeプロパティに依存します。
fetch の結果は Promise<Response> なので、直接返すことができます。
export function getObject() {
return fetch("https://bun.com");
}関数とほとんどのクラスのインスタンス(上記のものを除く)は直列化できません。
export function getText(url: string) {
// これは機能しません!
return () => {};
}引数
マクロは入力を受け入れられますが、限られた場合のみです。値は静的に既知である必要があります。たとえば、次は許可されていません:
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("The page is ", text.length, " characters long");
}ただし、foo の値がバンドル時に既知の場合(たとえば、定数または別のマクロの結果である場合)、許可されます:
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("The page is", text.length, "characters long");
}これは次のように出力されます:
function howLong() {
console.log("The page is", 1322, "characters long");
}
export { howLong };例
最新の git コミットハッシュを埋め込む
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 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);バンドル時に fetch() リクエストを実行
この例では、fetch() を使用して発信 HTTP リクエストを作成し、HTMLRewriter を使用して HTML レスポンスを解析し、バンドル時にタイトルとメタタグを含むオブジェクトを返します。
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 関数はバンドル時に消去され、関数呼び出しの結果に置き換えられます。これは、fetch リクエストがバンドル時に発生し、結果がバンドルに埋め込まれることを意味します。また、到達不能なため、エラーをスローするブランチも削除されます。
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>
);
};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>
);
};