Bun 提供了一个通用的插件 API,可用于扩展运行时和 打包器。
插件拦截导入并执行自定义加载逻辑:读取文件、转译代码等。它们可以用于添加对其他文件类型的支持,例如 .scss
或 .yaml
。在 Bun 打包器的上下文中,插件可以用于实现框架级功能,如 CSS 提取、宏和客户端-服务器代码共置。
用法
插件被定义为简单的 JavaScript 对象,包含一个 name
属性和一个 setup
函数。使用 Bun 的 plugin
函数注册插件。
import { plugin, type BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Custom loader",
setup(build) {
// implementation
},
};
plugin(myPlugin);
插件必须在任何其他代码运行之前加载!为了实现这一点,请在你的 bunfig.toml
中使用 preload
选项。Bun 会在运行文件之前自动加载在 preload
中指定的文件/模块。
preload = ["./myPlugin.ts"]
在 bun test
之前预加载文件
[test]
preload = ["./myPlugin.ts"]
第三方插件
按照惯例,旨在被使用的第三方插件应导出一个工厂函数,该函数接受一些配置并返回一个插件对象。
import { plugin } from "bun";
import fooPlugin from "bun-plugin-foo";
plugin(
fooPlugin({
// configuration
}),
);
Bun 的插件 API 大致基于 esbuild。仅实现了 esbuild API 的一个子集,但一些 esbuild 插件在 Bun 中“开箱即用”,例如官方的 MDX loader
import { plugin } from "bun";
import mdx from "@mdx-js/esbuild";
plugin(mdx());
加载器
插件主要用于使用加载器扩展 Bun 以支持其他文件类型。让我们看一个简单的插件,它为 .yaml
文件实现了一个加载器。
import { plugin } from "bun";
await plugin({
name: "YAML",
async setup(build) {
const { load } = await import("js-yaml");
// when a .yaml file is imported...
build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {
// read and parse the file
const text = await Bun.file(args.path).text();
const exports = load(text) as Record<string, any>;
// and returns it as a module
return {
exports,
loader: "object", // special loader for JS objects
};
});
},
});
在 preload
中注册此文件
preload = ["./yamlPlugin.ts"]
一旦插件注册,.yaml
和 .yml
文件就可以直接导入。
import data from "./data.yml"
console.log(data);
name: Fast X
releaseYear: 2023
请注意,返回的对象具有 loader
属性。这告诉 Bun 应该使用其内部加载器中的哪一个来处理结果。即使我们正在为 .yaml
实现加载器,结果仍然必须是 Bun 的内置加载器之一可以理解的。它始终是加载器。
在这种情况下,我们使用 "object"
——一个内置加载器(旨在供插件使用),它将纯 JavaScript 对象转换为等效的 ES 模块。支持任何 Bun 的内置加载器;Bun 内部也使用这些相同的加载器来处理各种类型的文件。下表是一个快速参考;有关完整文档,请参阅 打包器 > 加载器。
加载器 | 扩展名 | 输出 |
---|---|---|
js | .mjs .cjs | 转译为 JavaScript 文件 |
jsx | .js .jsx | 转换 JSX 然后转译 |
ts | .ts .mts .cts | 转换 TypeScript 然后转译 |
tsx | .tsx | 转换 TypeScript、JSX,然后转译 |
toml | .toml | 使用 Bun 的内置 TOML 解析器解析 |
json | .json | 使用 Bun 的内置 JSON 解析器解析 |
napi | .node | 导入原生 Node.js 插件 |
wasm | .wasm | 导入原生 Node.js 插件 |
object | none | 一个特殊的加载器,旨在供插件使用,它将纯 JavaScript 对象转换为等效的 ES 模块。对象中的每个键都对应于一个命名导出。 |
加载 YAML 文件很有用,但插件支持的功能不仅仅是数据加载。让我们看一个插件,它使 Bun 可以导入 *.svelte
文件。
import { plugin } from "bun";
await plugin({
name: "svelte loader",
async setup(build) {
const { compile } = await import("svelte/compiler");
// when a .svelte file is imported...
build.onLoad({ filter: /\.svelte$/ }, async ({ path }) => {
// read and compile it with the Svelte compiler
const file = await Bun.file(path).text();
const contents = compile(file, {
filename: path,
generate: "ssr",
}).js.code;
// and return the compiled source code as "js"
return {
contents,
loader: "js",
};
});
},
});
注意:在生产环境中,你可能需要缓存编译后的输出并包含额外的错误处理。
从 build.onLoad
返回的对象在 contents
中包含编译后的源代码,并将 "js"
指定为其加载器。这告诉 Bun 将返回的 contents
视为 JavaScript 模块,并使用 Bun 的内置 js
加载器对其进行转译。
有了这个插件,Svelte 组件现在可以直接导入和使用。
import "./sveltePlugin.ts";
import MySvelteComponent from "./component.svelte";
console.log(MySvelteComponent.render());
虚拟模块
此功能目前仅在运行时通过 Bun.plugin
提供,并且尚不支持打包器,但你可以使用 onResolve
和 onLoad
模拟该行为。
要在运行时创建虚拟模块,请在 Bun.plugin
的 setup
函数中使用 builder.module(specifier, callback)
。
例如
import { plugin } from "bun";
plugin({
name: "my-virtual-module",
setup(build) {
build.module(
// The specifier, which can be any string - except a built-in, such as "buffer"
"my-transpiled-virtual-module",
// The callback to run when the module is imported or required for the first time
() => {
return {
contents: "console.log('hello world!')",
loader: "js",
};
},
);
build.module("my-object-virtual-module", () => {
return {
exports: {
foo: "bar",
},
loader: "object",
};
});
},
});
// Sometime later
// All of these work
import "my-transpiled-virtual-module";
require("my-transpiled-virtual-module");
await import("my-transpiled-virtual-module");
require.resolve("my-transpiled-virtual-module");
import { foo } from "my-object-virtual-module";
const object = require("my-object-virtual-module");
await import("my-object-virtual-module");
require.resolve("my-object-virtual-module");
覆盖现有模块
你还可以使用 build.module
覆盖现有模块。
import { plugin } from "bun";
build.module("my-object-virtual-module", () => {
return {
exports: {
foo: "bar",
},
loader: "object",
};
});
require("my-object-virtual-module"); // { foo: "bar" }
await import("my-object-virtual-module"); // { foo: "bar" }
build.module("my-object-virtual-module", () => {
return {
exports: {
baz: "quix",
},
loader: "object",
};
});
require("my-object-virtual-module"); // { baz: "quix" }
await import("my-object-virtual-module"); // { baz: "quix" }
读取或修改配置
插件可以使用 build.config
读取和写入 构建配置。
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "demo",
setup(build) {
console.log(build.config.sourcemap); // "external"
build.config.minify = true; // enable minification
// `plugins` is readonly
console.log(`Number of plugins: ${build.config.plugins.length}`);
},
},
],
});
注意:插件生命周期回调 (onStart()
、onResolve()
等) 在 setup()
函数中无法修改 build.config
对象。 如果你想修改 build.config
,你必须直接在 setup()
函数中进行操作
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "demo",
setup(build) {
// ✅ good! modifying it directly in the setup() function
build.config.minify = true;
build.onStart(() => {
// 🚫 uh-oh! this won't work!
build.config.minify = false;
});
},
},
],
});
生命周期钩子
插件可以注册回调,以便在 bundle 生命周期的各个点运行
onStart()
:在打包器启动 bundle 后运行一次onResolve()
:在模块解析之前运行onLoad()
:在模块加载之前运行。
参考
类型的大致概述 (完整类型定义请参考 Bun 的 bun.d.ts
)
namespace Bun {
function plugin(plugin: {
name: string;
setup: (build: PluginBuilder) => void;
}): void;
}
type PluginBuilder = {
onStart(callback: () => void): void;
onResolve: (
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string; importer: string }) => {
path: string;
namespace?: string;
} | void,
) => void;
onLoad: (
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
) => void;
config: BuildConfig;
};
type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml" | "object";
命名空间
onLoad
和 onResolve
接受一个可选的 namespace
字符串。什么是命名空间?
每个模块都有一个命名空间。命名空间用于在转译代码中为导入添加前缀;例如,具有 filter: /\.yaml$/
和 namespace: "yaml:"
的加载器会将来自 ./myfile.yaml
的导入转换为 yaml:./myfile.yaml
。
默认命名空间是 "file"
,无需指定它,例如:import myModule from "./my-module.ts"
与 import myModule from "file:./my-module.ts"
相同。
其他常见的命名空间有
"bun"
:用于 Bun 特有的模块 (例如"bun:test"
,"bun:sqlite"
)"node"
:用于 Node.js 模块 (例如"node:fs"
,"node:path"
)
onStart
onStart(callback: () => void): Promise<void> | void;
注册一个回调,以便在打包器启动新 bundle 时运行。
import { plugin } from "bun";
plugin({
name: "onStart example",
setup(build) {
build.onStart(() => {
console.log("Bundle started!");
});
},
});
回调可以返回一个 Promise
。在 bundle 进程初始化后,打包器会等待所有 onStart()
回调完成,然后继续。
例如
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "Sleep for 10 seconds",
setup(build) {
build.onStart(async () => {
await Bunlog.sleep(10_000);
});
},
},
{
name: "Log bundle time to a file",
setup(build) {
build.onStart(async () => {
const now = Date.now();
await Bun.$`echo ${now} > bundle-time.txt`;
});
},
},
],
});
在上面的示例中,Bun 将等待第一个 onStart()
(休眠 10 秒) 完成,以及 第二个 onStart()
(将 bundle 时间写入文件)。
请注意,onStart()
回调 (像每个其他生命周期回调一样) 无法修改 build.config
对象。 如果你想修改 build.config
,你必须直接在 setup()
函数中进行操作。
onResolve
onResolve(
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string; importer: string }) => {
path: string;
namespace?: string;
} | void,
): void;
为了打包你的项目,Bun 会遍历项目中所有模块的依赖树。对于每个导入的模块,Bun 实际上必须找到并读取该模块。“查找”部分称为“解析”模块。
onResolve()
插件生命周期回调允许你配置模块的解析方式。
onResolve()
的第一个参数是一个对象,该对象具有 filter
和 namespace
属性。 filter 是一个正则表达式,它在导入字符串上运行。实际上,这些允许你过滤你的自定义解析逻辑将应用于哪些模块。
onResolve()
的第二个参数是一个回调,对于 Bun 找到的每个与第一个参数中定义的 filter
和 namespace
匹配的模块导入,都会运行该回调。
回调接收匹配模块的路径作为输入。回调可以返回模块的新路径。Bun 将读取新路径的内容并将其解析为模块。
例如,将所有对 images/
的导入重定向到 ./public/images/
import { plugin } from "bun";
plugin({
name: "onResolve example",
setup(build) {
build.onResolve({ filter: /.*/, namespace: "file" }, args => {
if (args.path.startsWith("images/")) {
return {
path: args.path.replace("images/", "./public/images/"),
};
}
});
},
});
onLoad
onLoad(
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
): void;
在 Bun 的打包器解析模块后,它需要读取模块的内容并解析它。
onLoad()
插件生命周期回调允许你在模块被 Bun 读取和解析之前修改模块的内容。
与 onResolve()
类似,onLoad()
的第一个参数允许你过滤此 onLoad()
调用将应用于哪些模块。
onLoad()
的第二个参数是一个回调,对于每个匹配的模块,在 Bun 将模块内容加载到内存之前,都会运行该回调。
此回调接收匹配模块的路径、模块的导入器 (导入模块的模块)、模块的命名空间和模块的种类作为输入。
回调可以为模块返回新的 contents
字符串以及新的 loader
。
例如
import { plugin } from "bun";
plugin({
name: "env plugin",
setup(build) {
build.onLoad({ filter: /env/, namespace: "file" }, args => {
return {
contents: `export default ${JSON.stringify(process.env)}`,
loader: "js",
};
});
},
});
此插件将把所有 import env from "env"
形式的导入转换为导出当前环境变量的 JavaScript 模块。