宏是一种在打包时执行 JavaScript 函数的机制。这些函数返回的值将直接内联到您的包中。
作为一个简单的例子,考虑这个返回随机数的函数。
export function random() {
return Math.random();
}
这只是一个普通文件中的常规函数,但我们可以像这样使用它作为宏
import { random } from './random.ts' with { type: 'macro' };
console.log(`Your random number is ${random()}`);
注意 — 宏使用 import 属性 语法进行指示。如果您以前从未见过这种语法,这是一个 Stage 3 TC39 提案,它允许您为 import
语句附加其他元数据。
现在我们将使用 bun build
来打包此文件。打包后的文件将打印到 stdout。
bun build ./cli.tsx
console.log(`Your random number is ${0.6805550949689833}`);
如您所见,random
函数的源代码在包中不复存在。相反,它在打包期间执行,函数调用 (random()
) 被替换为函数的结果。由于源代码永远不会包含在包中,因此宏可以安全地执行特权操作,例如从数据库读取。
何时使用宏
如果您有许多用于小型任务的构建脚本,而您原本可能只需要一个一次性的构建脚本,那么打包时执行代码会更容易维护。它与您的其他代码位于同一位置,与整个构建过程同时运行,并且会自动并行化。如果它失败,构建也会失败。
但是,如果您发现自己在打包时运行大量代码,可以考虑改用服务器。
导入属性
Bun 宏是使用以下任一方式注解的导入语句:
with { type: 'macro' }
— 一个 导入属性,一个 Stage 3 ECMA Scrdassert { type: 'macro' }
— 一个导入断言,是导入属性的早期版本,现已被弃用(但已被许多浏览器和运行时支持)。
安全注意事项
宏必须显式地使用 { 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"
导出条件,专门为宏环境提供您软件包的特殊版本。
{
"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 的转译器看到宏导入时,它会在转译器内部使用 Bun 的 JavaScript 运行时调用函数,并将返回值从 JavaScript 转换为 AST 节点。这些 JavaScript 函数在打包时执行,而不是在运行时执行。
宏在转译器的访问阶段同步执行—在插件之前,并且在转译器生成 AST 之前。它们按照导入的顺序执行。转译器将在宏完成执行后再继续。转译器还会 await
宏返回的任何 Promise
。
Bun 的打包器是多线程的。因此,宏会在多个生成的 JavaScript "worker" 中并行执行。
死代码消除
打包器在运行和内联宏之后执行死代码消除。因此,给定以下宏
export function returnFalse() {
return false;
}
...然后打包以下文件将生成一个空包,前提是启用了 minify 语法选项。
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 的转译器将自动 await
Promise
并内联结果。
export async function getText() {
return "async value";
}
转译器实现了用于序列化常见数据格式(如 Response
、Blob
、TypedArray
)的特殊逻辑。
TypedArray
:解析为 base64 编码的字符串。Response
:Bun 将读取Content-Type
并相应地进行序列化;例如,type
为application/json
的Response
将自动解析为对象,而text/plain
将作为字符串内联。type
未识别或为undefined
的响应将进行 base-64 编码。Blob
:与Response
类似,序列化取决于type
属性。
fetch
的结果是 Promise<Response>
,因此可以直接返回。
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 hash
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 响应,并返回一个包含标题和 meta 标签的对象——所有这些都在打包时完成。
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>;
};
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 };