Skip to content

Bun предоставляет универсальный API плагинов, который можно использовать для расширения как среды выполнения, так и сборщика.

Плагины перехватывают импорты и выполняют пользовательскую логику загрузки: чтение файлов, транспиляцию кода и т.д. Они могут использоваться для добавления поддержки дополнительных типов файлов, таких как .scss или .yaml. В контексте сборщика Bun плагины могут использоваться для реализации функций на уровне фреймворка, таких как извлечение CSS, макросы и совместное размещение кода клиента и сервера.

Хуки жизненного цикла

Плагины могут регистрировать обратные вызовы для выполнения в различных точках жизненного цикла сборки:

  • onStart(): Выполняется один раз, когда сборщик начинает сборку
  • onResolve(): Выполняется перед разрешением модуля
  • onLoad(): Выполняется перед загрузкой модуля.
  • onBeforeParse(): Выполняет нативные аддоны с нулевым копированием в потоке парсера до разбора файла.

Ссылка

Приблизительный обзор типов (пожалуйста, обратитесь к bun.d.ts Bun для полных определений типов):

ts
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.

tsx
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "Custom loader",
  setup(build) {
    // реализация
  },
};

Этот плагин может быть передан в массив plugins при вызове Bun.build.

ts
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

ts
onStart(callback: () => void): Promise<void> | void;

Регистрирует обратный вызов для выполнения, когда сборщик начинает новую сборку.

ts
import { plugin } from "bun";

plugin({
  name: "onStart example",

  setup(build) {
    build.onStart(() => {
      console.log("Bundle started!");
    });
  },
});

Обратный вызов может возвращать Promise. После инициализации процесса сборки сборщик ждёт завершения всех обратных вызовов onStart(), прежде чем продолжить.

Например:

ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "Sleep for 10 seconds",
      setup(build) {
        build.onStart(async () => {
          await Bun.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() (запись времени сборки в файл).

Обратите внимание, что обратные вызовы onStart() (как и любой другой обратный вызов жизненного цикла) не имеют возможности изменять объект build.config. Если вы хотите изменить build.config, вы должны сделать это непосредственно в функции setup().

onResolve

ts
onResolve(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; importer: string }) => {
    path: string;
    namespace?: string;
  } | void,
): void;

Для сборки вашего проекта Bun проходит по дереву зависимостей всех модулей в вашем проекте. Для каждого импортируемого модуля Bun должен найти и прочитать этот модуль. Часть "поиска" известна как "разрешение" модуля.

Обратный вызов жизненного цикла плагина onResolve() позволяет настроить, как разрешается модуль.

Первый аргумент onResolve() — это объект со свойствами filter и namespace. Фильтр — это регулярное выражение, которое запускается на строке импорта. По сути, это позволяет фильтровать, к каким модулям будет применяться ваша пользовательская логика разрешения.

Второй аргумент onResolve() — это обратный вызов, который выполняется для каждого импорта модуля, который Bun находит и который соответствует filter и namespace, определённым в первом аргументе.

Обратный вызов получает в качестве входных данных путь к соответствующему модулю. Обратный вызов может возвращать новый путь для модуля. Bun прочитает содержимое нового пути и разберёт его как модуль.

Например, перенаправление всех импортов в images/ в ./public/images/:

ts
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

ts
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 загрузит содержимое модуля в память.

Этот обратный вызов получает в качестве входных данных путь к соответствующему модулю, импортер модуля (модуль, который импортировал модуль), пространство имён модуля и kind модуля.

Обратный вызов может возвращать новую строку contents для модуля, а также новый loader.

Например:

ts
import { plugin } from "bun";

const envPlugin: BunPlugin = {
  name: "env plugin",
  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 до загрузки всех других модулей.

Это полезно для возврата содержимого модуля, который зависит от других модулей.

Пример: отслеживание и отчёт о неиспользуемых экспортах
ts
import { plugin } from "bun";

plugin({
  name: "track imports",
  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",
      };
    });
  },
});

Обратите внимание, что функция .defer() в настоящее время имеет ограничение: она может быть вызвана только один раз для каждого обратного вызова onLoad.

Нативные плагины

Одной из причин, по которой сборщик Bun так быстр, является то, что он написан на нативном коде и использует многопоточность для параллельной загрузки и разбора модулей.

Однако одним из ограничений плагинов, написанных на JavaScript, является то, что сам JavaScript однопоточный.

Нативные плагины написаны как модули NAPI и могут выполняться на нескольких потоках. Это позволяет нативным плагинам работать намного быстрее, чем плагины на JavaScript.

Кроме того, нативные плагины могут пропускать ненужную работу, такую как преобразование UTF-8 -> UTF-16, необходимое для передачи строк в JavaScript.

Это следующие хуки жизненного цикла, доступные для нативных плагинов:

  • onBeforeParse(): Вызывается в любом потоке до разбора файла сборщиком Bun.

Нативные плагины — это модули NAPI, которые предоставляют хуки жизненного цикла как функции C ABI.

Для создания нативного плагина вы должны экспортировать функцию C ABI, которая соответствует сигнатуре нативного хука жизненного цикла, который вы хотите реализовать.

Создание нативного плагина на Rust

Нативные плагины — это модули NAPI, которые предоставляют хуки жизненного цикла как функции C ABI.

Для создания нативного плагина вы должны экспортировать функцию C ABI, которая соответствует сигнатуре нативного хука жизненного цикла, который вы хотите реализовать.

bash
bun add -g @napi-rs/cli
napi new

Затем установите этот крейт:

bash
cargo add bun-native-plugin

Теперь внутри файла lib.rs мы будем использовать макрос bun_native_plugin::bun proc для определения функции, которая реализует наш нативный плагин.

Вот пример реализации хука onBeforeParse:

rs
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():

typescript
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

ts
onBeforeParse(
  args: { filter: RegExp; namespace?: string },
  callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;

Этот хук жизненного цикла выполняется непосредственно перед разбором файла сборщиком Bun.

В качестве входных данных он получает содержимое файла и может опционально возвращать новый исходный код.

Этот обратный вызов может вызываться из любого потока, поэтому реализация модуля napi должна быть потокобезопасной.

Bun от www.bunjs.com.cn