宏是一种在打包时运行 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' }— 导入断言,早期版本的导入属性已被放弃(但已得到许多浏览器和运行时的支持)
安全考虑
宏必须显式使用 { 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,而第二个将被 Bun 的打包器解析为 ./node_modules/my-package/index.macro.js。
执行
当 Bun 的转译器看到宏导入时,它使用 Bun 的 JavaScript 运行时在转译器内部调用函数,并将返回值从 JavaScript 转换为 AST 节点。这些 JavaScript 函数在打包时调用,而不是运行时。
宏在访问阶段在转译器中同步执行——在插件之前和转译器生成 AST 之前。它们按导入顺序执行。转译器将等待宏完成执行后再继续。转译器还将等待宏返回的任何 Promise。
Bun 的打包器是多线程的。因此,宏在多个生成的 JavaScript "worker" 中并行执行。
死代码消除
打包器在运行和内联宏后执行死代码消除。因此给定以下宏:
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>
);
};