Bun

宏是一种在打包时运行 JavaScript 函数的机制。从这些函数返回的值直接内联到您的 bundle 中。

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

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

这只是一个常规文件中的常规函数,但我们可以像这样将其用作宏

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

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

注意 — 宏使用导入属性语法指示。如果您以前没有见过这种语法,这是一个 Stage 3 TC39 提案,允许您将额外的元数据附加到 import 语句。

现在我们将使用 bun build 打包此文件。打包后的文件将打印到 stdout。

bun build ./cli.tsx
console.log(`Your random number is ${0.6805550949689833}`);

正如您所见,random 函数的源代码在 bundle 中任何地方都没有出现。相反,它在打包期间执行,函数调用 (random()) 被函数的结果替换。由于源代码永远不会包含在 bundle 中,宏可以安全地执行特权操作,例如从数据库读取。

何时使用宏

如果您有几个用于小事物的构建脚本,否则您将有一个一次性构建脚本,则打包时代码执行可能更易于维护。它与代码的其余部分一起存在,它与构建的其余部分一起运行,它是自动并行化的,如果它失败,构建也会失败。

如果您发现自己在打包时运行大量代码,请考虑运行服务器。

导入属性

Bun 宏是使用以下任一方式注释的 import 语句

安全注意事项

宏必须显式地使用 { type: "macro" } 导入才能在打包时执行。这些导入如果未被调用则无效,这与可能具有副作用的常规 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" 导出条件 以提供专用于宏环境的软件包的特殊版本。

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

通过此配置,用户可以使用相同的导入说明符在运行时或打包时使用您的软件包

import pkg from "my-package";                            // runtime import
import {macro} from "my-package" with { type: "macro" }; // macro import

第一个导入将解析为 ./node_modules/my-package/index.js,而第二个将由 Bun 的打包器解析为 ./node_modules/my-package/index.macro.js

执行

当 Bun 的 transpiler 看到宏导入时,它会使用 Bun 的 JavaScript 运行时在 transpiler 内部调用该函数,并将 JavaScript 的返回值转换为 AST 节点。这些 JavaScript 函数在打包时而不是运行时调用。

宏在访问阶段(在插件之前和 transpiler 生成 AST 之前)在 transpiler 中同步执行。它们按照导入顺序执行。Transpiler 将等待宏完成执行,然后继续。Transpiler 还会 await 宏返回的任何 Promise

Bun 的打包器是多线程的。因此,宏在多个衍生的 JavaScript “worker” 中并行执行。

死代码消除

打包器在运行和内联宏之后执行死代码消除。因此,给定以下宏

returnFalse.ts
export function returnFalse() {
  return false;
}

...然后打包以下文件将生成一个空 bundle,前提是启用了 minify 语法选项。

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

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

可序列化

Bun 的 transpiler 需要能够序列化宏的结果,以便可以将其内联到 AST 中。支持所有 JSON 兼容的数据结构

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

宏可以是异步的,或者返回 Promise 实例。Bun 的 transpiler 将自动 await Promise 并内联结果。

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

Transpiler 实现了用于序列化常见数据格式(如 ResponseBlobTypedArray)的特殊逻辑。

  • TypedArray:解析为 base64 编码的字符串。
  • Response:Bun 将读取 Content-Type 并相应地进行序列化;例如,类型为 application/jsonResponse 将自动解析为对象,而 text/plain 将作为字符串内联。具有无法识别或 undefined type 的 Response 将进行 base-64 编码。
  • Blob:与 Response 一样,序列化取决于 type 属性。

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

macro.ts
export function getObject() {
  return fetch("https://bun.net.cn")
}

函数和大多数类的实例(上面提到的类除外)是不可序列化的。

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

但是,如果在打包时知道 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 哈希值

getGitCommitHash.ts
getGitCommitHash.ts
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?” 好吧,您也可以这样做。但是您可以使用环境变量做到这一点吗?

在打包时进行 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 请求发生在打包时,结果嵌入在 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>;
};
{headTags.title} - Bun 中文 ; };`).then(()=>{clicker.querySelector(".clipboard").classList.add("hidden");clicker.querySelector(".check").classList.remove("hidden");setTimeout(()=>{clicker.querySelector(".clipboard").classList.remove("hidden");clicker.querySelector(".check").classList.add("hidden")},2500)})})});
输出
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 };