> ## Documentation Index
> Fetch the complete documentation index at: https://bun.ll1025.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# Macros

> 在打包时运行 JavaScript 函数——Bun 宏

宏是在打包时运行的 JavaScript 函数。它们的返回值会直接内联到你的打包产物中。

作为一个示例，考虑这个返回随机数的函数。

```ts title="random.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export function random() {
  return Math.random();
}
```

这是一个普通文件中的普通函数，但你可以将其用作宏：

```tsx title="cli.tsx" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { random } from "./random.ts" with { type: "macro" };

console.log(`你的随机数是 ${random()}`);
```

<Note>
  宏使用导入属性（import attribute）语法标记，这是 TC39 Stage 3 提案，用于向 import 语句附加额外元数据。
</Note>

使用 `bun build` 打包该文件。打包后的文件会打印到标准输出。

```bash terminal icon="terminal" theme={null}
bun build ./cli.tsx
```

```js theme={null}
console.log(`你的随机数是 ${0.6805550949689833}`);
```

`random` 函数的源代码不会出现在打包产物中。相反，它在打包期间运行，调用（`random()`）被替换为其结果。由于源代码永远不会包含在打包产物中，因此宏可以安全地执行特权操作，比如读取数据库。

## 何时使用宏

对于那些你本来会编写一次性构建脚本的小事情，打包时代码执行更容易维护。它与你其余代码生活在一起，与构建一起运行，自动并行化，并且如果失败了，构建也会失败。

但如果你发现自己在打包时运行了大量代码，不妨考虑运行一个服务器。

## 导入属性

宏是用以下任一方式注解的 import 语句：

* `with { type: 'macro' }`——导入属性，一个 Stage 3 ECMAScript 提案
* `assert { type: 'macro' }`——导入断言，导入属性的早期版本，现已被放弃（但已被许多浏览器和运行时支持）

## 安全考虑

宏必须显式使用 `{ type: "macro" }` 导入才能在打包时运行。如果不调用它们，这些导入不会产生任何效果，这与可能具有副作用的常规 JavaScript 导入不同。

你可以使用 `--no-macros` 标志完全禁用宏。它会产生如下构建错误：

```
error: Macros 已被禁用

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

为了减少恶意包的攻击面，宏不能从 `node_modules/**/*` 内部调用。如果某个包尝试调用宏，你会看到如下错误：

```
error: 出于安全原因，宏不能从 node_modules 中运行。

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

你的应用代码仍然可以从 `node_modules` 导入宏并调用它们。

```ts title="cli.tsx" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { macro } from "some-package" with { type: "macro" };

macro();
```

## 导出条件 "macro"

当向 npm 或其他包注册表发布包含宏的库时，使用 `"macro"` 导出条件为宏环境提供专有版本。

```json title="package.json" icon="file-json" theme={null}
{
  "name": "my-package",
  "exports": {
    "import": "./index.js",
    "require": "./index.js",
    "default": "./index.js",
    "macro": "./index.macro.js"
  }
}
```

使用此配置，用户可以使用相同的导入标识符在运行时或打包时消费你的包：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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 节点。

宏在转译器的访问（visiting）阶段同步运行，在插件之前、在转译器生成 AST 之前。它们按导入的顺序运行。转译器会等待每个宏完成后再继续，并 await 宏返回的任何 Promise。

Bun 的打包器是多线程的，因此宏在多个生成的 JavaScript "worker" 中并行执行。

## 死代码消除

打包器在运行并内联宏之后执行死代码消除。给定以下宏：

```ts title="returnFalse.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export function returnFalse() {
  return false;
}
```

……在启用了 minify 语法选项的情况下，打包以下文件会生成一个空的打包产物。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };

if (returnFalse()) {
  console.log("这段代码已被消除");
}
```

## 可序列化性

Bun 的转译器必须能够序列化宏的结果，以便将其内联到 AST 中。所有兼容 JSON 的数据结构都支持：

```ts title="macro.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export function getObject() {
  return {
    foo: "bar",
    baz: 123,
    array: [1, 2, { nested: "value" }],
  };
}
```

宏可以是异步的，或者返回 Promise 实例。Bun 的转译器会 await 该 Promise 并内联结果。

```ts title="macro.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export async function getText() {
  return "async value";
}
```

转译器对常见数据格式实现了特殊序列化逻辑，如 `Response` 和 `Blob`。

* **Response**：Bun 读取 `Content-Type` 并相应序列化；例如，类型为 `application/json` 的 Response 会被解析为对象，`text/plain` 会被内联为字符串。类型无法识别或未定义时，Response 会被 base64 编码。
* **Blob**：与 Response 类似，序列化取决于 `type` 属性。

`fetch` 的结果是 `Promise<Response>`，因此可以直接返回。

```ts title="macro.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export function getObject() {
  return fetch("https://bun.com");
}
```

函数和大多数类的实例（除了前面列出的那些）是不可序列化的。

```ts title="macro.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export function getText(url: string) {
  // 这行不通！
  return () => {};
}
```

## 参数

宏可以接受输入，但仅限有限的情况。该值必须是静态已知的。例如，以下是不允许的：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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` 的值在打包时是已知的（例如，如果它是常量或另一个宏的结果），那么就是允许的：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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, "个字符长");
}
```

输出：

```js theme={null}
function howLong() {
  console.log("页面有", 1322, "个字符长");
}
export { howLong };
```

## 示例

### 嵌入最新的 Git 提交哈希

```ts title="getGitCommitHash.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
export function getGitCommitHash() {
  const { stdout } = Bun.spawnSync({
    cmd: ["git", "rev-parse", "HEAD"],
    stdout: "pipe",
  });

  return stdout.toString();
}
```

当你构建时，`getGitCommitHash` 调用会被替换为调用该函数的结果：

<CodeGroup>
  ```ts input theme={null}
  import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };

  console.log(`当前的 Git 提交哈希是 ${getGitCommitHash()}`);
  ```

  ```ts output theme={null}
  console.log(`当前的 Git 提交哈希是 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);
  ```
</CodeGroup>

<Info>
  你可能会想"为什么不用 `process.env.GIT_COMMIT_HASH`？"嗯，你也可以那样做。但你能用环境变量做到这样吗？
</Info>

### 在打包时发起 fetch() 请求

这个示例在打包时使用 `fetch()` 发起 HTTP 请求，使用 `HTMLRewriter` 解析 HTML 响应，并返回包含标题和 meta 标签的对象。

```ts title="meta.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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 请求在打包时发生，结果嵌入到打包产物中。抛出错误的分支也会被消除，因为它是不可达的。

<CodeGroup>
  ```jsx input theme={null}
  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>
    );
  };
  ```

  ```jsx output theme={null}
  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>
    );
  };
  ```
</CodeGroup>
