Bun предоставляет универсальный API плагинов который может использоваться для расширения как времени выполнения так и бандлера.
Плагины перехватывают импорты и выполняют пользовательскую логику загрузки: чтение файлов транспиляцию кода и т.д. Они могут использоваться для добавления поддержки дополнительных типов файлов таких как .scss или .yaml. В контексте бандлера Bun плагины могут использоваться для реализации функций уровня фреймворка таких как извлечение CSS макросы и совместное размещение кода клиента и сервера.
Хуки жизненного цикла
Плагины могут регистрировать обратные вызовы для запуска в различных точках жизненного цикла бандла:
onStart(): Запускается один раз когда бандлер начал сборку бандлаonResolve(): Запускается перед загрузкой модуляonLoad(): Запускается перед загрузкой модуляonBeforeParse(): Запускает нативные аддоны с нулевым копированием в потоке парсера перед разбором файла
Справочник
Приблизительный обзор типов (пожалуйста обратитесь к bun.d.ts от Bun для полных определений типов):
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 },
defer: () => Promise<void>,
callback: (args: { path: string }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
) => void;
config: BuildConfig;
};
type Loader =
| "js"
| "jsx"
| "ts"
| "tsx"
| "json"
| "jsonc"
| "toml"
| "yaml"
| "file"
| "napi"
| "wasm"
| "text"
| "css"
| "html";Использование
Плагин определяется как простой объект JavaScript содержащий свойство name и функцию setup.
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Пользовательский загрузчик",
setup(build) {
// реализация
},
};Этот плагин может быть передан в массив plugins при вызове Bun.build.
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
plugins: [myPlugin],
});Жизненный цикл плагина
Пространства имен
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;Регистрирует обратный вызов для запуска когда бандлер начинает новый бандл.
import { plugin } from "bun";
plugin({
name: "onStart пример",
setup(build) {
build.onStart(() => {
console.log("Сборка бандла началась!");
});
},
});Обратный вызов может возвращать Promise. После инициализации процесса сборки бандлер ждет завершения всех обратных вызовов onStart() перед продолжением.
Например:
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "Сон на 10 секунд",
setup(build) {
build.onStart(async () => {
await Bun.sleep(10_000);
});
},
},
{
name: "Запись времени сборки в файл",
setup(build) {
build.onStart(async () => {
const now = Date.now();
await Bun.$`echo ${now} > bundle-time.txt`;
});
},
},
],
});В приведенном выше примере Bun ждет завершения первого onStart() (сон на 10 секунд) а также второго onStart() (запись времени сборки в файл).
NOTE
Обратные вызовы `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 находит соответствующим фильтру и пространству имен определенному в первом аргументе.
Обратный вызов получает в качестве входных данных путь к соответствующему модулю. Обратный вызов может возвращать новый путь для модуля. Bun прочитает содержимое нового пути и разберет его как модуль.
Например перенаправление всех импортов в images/ в ./public/images/:
import { plugin } from "bun";
plugin({
name: "onResolve пример",
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 },
defer: () => Promise<void>,
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";
const envPlugin: BunPlugin = {
name: "env плагин",
setup(build) {
build.onLoad({ filter: /env/, namespace: "file" }, args => {
return {
contents: `export default ${JSON.stringify(process.env)}`,
loader: "js",
};
});
},
};
Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
plugins: [envPlugin],
});
// import env from "env"
// env.FOO === "bar"Этот плагин преобразует все импорты вида import env from "env" в JavaScript модуль который экспортирует текущие переменные окружения.
.defer()
Один из аргументов передаваемых в обратный вызов onLoad — это функция defer. Эта функция возвращает Promise который разрешается когда все другие модули были загружены.
Это позволяет вам отложить выполнение обратного вызова onLoad до тех пор пока все другие модули не будут загружены.
Это полезно для возврата содержимого модуля который зависит от других модулей.
Пример: отслеживание и отчет о неиспользуемых экспортах">
import { plugin } from "bun";
plugin({
name: "отслеживание импортов",
setup(build) {
const transpiler = new Bun.Transpiler();
let trackedImports: Record<string, number> = {};
// Каждый модуль который проходит через этот обратный вызов onLoad
// будет записывать свои импорты в `trackedImports`
build.onLoad({ filter: /\.ts/ }, async ({ path }) => {
const contents = await Bun.file(path).arrayBuffer();
const imports = transpiler.scanImports(contents);
for (const i of imports) {
trackedImports[i.path] = (trackedImports[i.path] || 0) + 1;
}
return undefined;
});
build.onLoad({ filter: /stats\.json/ }, async ({ defer }) => {
// Ждем загрузки всех файлов обеспечивая
// что каждый файл проходит через функцию `onLoad()` выше
// и их импорты отслеживаются
await defer();
// Вывод JSON содержащего статистику каждого импорта
return {
contents: `export default ${JSON.stringify(trackedImports)}`,
loader: "json",
};
});
},
});Нативные плагины
Одна из причин почему бандлер Bun такой быстрый — это то что он написан на нативном коде и использует многопоточность для загрузки и разбора модулей параллельно.
Однако одно из ограничений плагинов написанных на JavaScript — это то что сам JavaScript однопоточный.
Нативные плагины написаны как NAPI модули и могут запускаться в нескольких потоках. Это позволяет нативным плагинам работать намного быстрее чем JavaScript плагины.
Кроме того нативные плагины могут пропускать ненужную работу такую как преобразование UTF-8 -> UTF-16 необходимое для передачи строк в JavaScript.
Это следующие хуки жизненного цикла которые доступны для нативных плагинов:
onBeforeParse(): Вызывается в любом потоке перед разбором файла бандлером Bun.
Нативные плагины — это NAPI модули которые предоставляют хуки жизненного цикла как функции C ABI.
Для создания нативного плагина вы должны экспортировать функцию C ABI которая соответствует сигнатуре нативного хука жизненного цикла который вы хотите реализовать.
Создание нативного плагина на Rust
Нативные плагины — это NAPI модули которые предоставляют хуки жизненного цикла как функции C ABI.
Для создания нативного плагина вы должны экспортировать функцию C ABI которая соответствует сигнатуре нативного хука жизненного цикла который вы хотите реализовать.
bun add -g @napi-rs/cli
napi newЗатем установите этот крейт:
cargo add bun-native-pluginТеперь внутри файла lib.rs мы будем использовать proc-макрос bun_native_plugin::bun для определения функции которая реализует наш нативный плагин.
Вот пример реализующий хук onBeforeParse:
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;
/// Определяет плагин и его имя
define_bun_plugin!("replace-foo-with-bar");
/// Здесь мы реализуем `onBeforeParse` с кодом который заменяет все вхождения
/// `foo` на `bar`.
///
/// Мы используем макрос #[bun] для генерации части шаблонного кода.
///
/// Аргумент функции (`handle: &mut OnBeforeParse`) говорит
/// макросу что эта функция реализует хук `onBeforeParse`.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
// Получаем исходный код ввода.
let input_source_code = handle.input_source_code()?;
// Получаем Loader для файла
let loader = handle.output_loader();
let output_source_code = input_source_code.replace("foo", "bar");
handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX);
Ok(())
}И для использования в Bun.build():
import myNativeAddon from "./my-native-addon";
Bun.build({
entrypoints: ["./app.tsx"],
plugins: [
{
name: "my-plugin",
setup(build) {
build.onBeforeParse(
{
namespace: "file",
filter: "**/*.tsx",
},
{
napiModule: myNativeAddon,
symbol: "replace_foo_with_bar",
// external: myNativeAddon.getSharedState()
},
);
},
},
],
});onBeforeParse
onBeforeParse(
args: { filter: RegExp; namespace?: string },
callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;Этот обратный вызов жизненного цикла запускается непосредственно перед разбором файла бандлером Bun.
В качестве входных данных он получает содержимое файла и может опционально возвращать новый исходный код.