Skip to content

マクロは、バンドル時に JavaScript 関数を実行するためのメカニズムです。これらの関数から返された値は、バンドルに直接インライン化されます。

簡単な例として、乱数を返すこの単純な関数を考えてください。

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

これは通常のファイル内の通常の関数ですが、次のようにマクロとして使用できます:

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

console.log(`Your random number is ${random()}`);

NOTE

マクロはインポート属性構文を使用して示されます。この構文を見たことがない場合、これは Stage 3 TC39 提案で、インポート文に追加のメタデータを添付できます。

次に、bun build でこのファイルをバンドルします。バンドルされたファイルは stdout に出力されます。

bash
bun build ./cli.tsx
js
console.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 からマクロをインポートして呼び出すことができます。

ts
import { macro } from "some-package" with { type: "macro" };

macro();

エクスポート条件 "macro"

マクロを含むライブラリを npm や他のパッケージレジストリに出荷する場合、"macro" エクスポート条件を使用して、マクロ環境専用のパッケージの特別なバージョンを提供します。

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

この構成により、ユーザーは同じインポート指定子を使用して、ランタイムまたはバンドル時にパッケージを使用できます:

ts
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 "ワーカー" 内で並列に実行されます。

デッドコード削除

バンドラーは、マクロを実行してインライン化した後にデッドコード削除を実行します。したがって、次のマクロが与えられた場合:

ts
export function returnFalse() {
  return false;
}

... 次のファイルをバンドルすると、ミニファイ構文オプションが有効になっている場合、空のバンドルが生成されます。

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

if (returnFalse()) {
  console.log("This code is eliminated");
}

直列化可能性

Bun のトランスパイラーは、マクロの結果を直列化して AST にインライン化できる必要があります。すべての JSON 互換のデータ構造がサポートされています:

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

マクロは非同期にすることも、Promise インスタンスを返すこともできます。Bun のトランスパイラーは自動的に Promise を待機して結果をインライン化します。

ts
export async function getText() {
  return "async value";
}

トランスパイラーは、ResponseBlobTypedArray などの一般的なデータ形式を直列化するための特別なロジックを実装しています。

  • TypedArray: base64 エンコードされた文字列に解決されます。
  • Response: Bun は Content-Type を読み取り、それに応じて直列化します。たとえば、タイプ application/json の Response は自動的にオブジェクトに解析され、text/plain は文字列としてインライン化されます。認識されないまたは未定義のタイプの Response は base-64 エンコードされます。
  • Blob: Response と同様に、type プロパティに依存します。

fetch の結果は Promise<Response> なので、直接返すことができます。

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

関数とほとんどのクラスのインスタンス(上記のものを除く)は直列化できません。

ts
export function getText(url: string) {
  // これは機能しません!
  return () => {};
}

引数

マクロは入力を受け入れられますが、限られた場合のみです。値は静的に既知である必要があります。たとえば、次は許可されていません:

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("The page is ", text.length, " characters long");
}

ただし、foo の値がバンドル時に既知の場合(たとえば、定数または別のマクロの結果である場合)、許可されます:

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("The page is", text.length, "characters long");
}

これは次のように出力されます:

js
function howLong() {
  console.log("The page is", 1322, "characters long");
}
export { howLong };

最新の git コミットハッシュを埋め込む

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

  return stdout.toString();
}

ビルドすると、getGitCommitHash は関数を呼び出した結果に置き換えられます:

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

console.log(`The current Git commit hash is ${getGitCommitHash()}`);
ts
console.log(`The current Git commit hash is 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

バンドル時に fetch() リクエストを実行

この例では、fetch() を使用して発信 HTTP リクエストを作成し、HTMLRewriter を使用して HTML レスポンスを解析し、バンドル時にタイトルとメタタグを含むオブジェクトを返します。

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;
}

extractMetaTags 関数はバンドル時に消去され、関数呼び出しの結果に置き換えられます。これは、fetch リクエストがバンドル時に発生し、結果がバンドルに埋め込まれることを意味します。また、到達不能なため、エラーをスローするブランチも削除されます。

jsx
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>
  );
};
jsx
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>
  );
};

Bun by www.bunjs.com.cn 編集