O Bun fornece uma API de plugin universal que pode ser usada para estender tanto o runtime quanto o bundler.
Plugins interceptam imports e realizam lógica de carregamento customizada: ler arquivos, transpilar código, etc. Eles podem ser usados para adicionar suporte para tipos de arquivo adicionais, como .scss ou .yaml. No contexto do bundler do Bun, plugins podem ser usados para implementar features de nível de framework como extração de CSS, macros e co-locação de código client-server.
Hooks de lifecycle
Plugins podem registrar callbacks para serem executados em vários pontos no lifecycle de um bundle:
onStart(): Executado uma vez quando o bundler inicia um bundleonResolve(): Executado antes de um módulo ser resolvidoonLoad(): Executado antes de um módulo ser carregadoonBeforeParse(): Executa addons nativos zero-copy na thread do parser antes de um arquivo ser parseado
Referência
Uma visão geral aproximada dos tipos (por favor consulte bun.d.ts do Bun para as definições de tipo completas):
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";Uso
Um plugin é definido como um objeto JavaScript simples contendo uma propriedade name e uma função setup.
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Custom loader",
setup(build) {
// implementação
},
};Este plugin pode ser passado para o array plugins ao chamar Bun.build.
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
plugins: [myPlugin],
});Lifecycle do plugin
Namespaces
onLoad e onResolve aceitam uma string namespace opcional. O que é um namespace?
Todo módulo tem um namespace. Namespaces são usados para prefixar o import em código transpilado; por exemplo, um loader com filter: /\.yaml$/ e namespace: "yaml:" transformará um import de ./myfile.yaml para yaml:./myfile.yaml.
O namespace padrão é "file" e não é necessário especificá-lo, por exemplo: import myModule from "./my-module.ts" é o mesmo que import myModule from "file:./my-module.ts".
Outros namespaces comuns são:
"bun": para módulos específicos do Bun (ex."bun:test","bun:sqlite")"node": para módulos Node.js (ex."node:fs","node:path")
onStart
onStart(callback: () => void): Promise<void> | void;Registra um callback para ser executado quando o bundler inicia um novo bundle.
import { plugin } from "bun";
plugin({
name: "onStart example",
setup(build) {
build.onStart(() => {
console.log("Bundle started!");
});
},
});O callback pode retornar uma Promise. Após o processo de bundle ter inicializado, o bundler espera até que todos os callbacks onStart() tenham completado antes de continuar.
Por exemplo:
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`;
});
},
},
],
});No exemplo acima, o Bun espera até que o primeiro onStart() (dormindo por 10 segundos) tenha completado, assim como o segundo onStart() (escrevendo o tempo de bundle em um arquivo).
NOTE
Callbacks `onStart()` (como todo outro callback de lifecycle) não têm a habilidade de modificar o objeto `build.config`. Se você quer fazer mutate em `build.config`, você deve fazê-lo diretamente na função `setup()`.onResolve
onResolve(
args: { filter: RegExp; namespace?: string },
callback: (args: { path: string; importer: string }) => {
path: string;
namespace?: string;
} | void,
): void;Para fazer bundle do seu projeto, o Bun percorre a árvore de dependências de todos os módulos no seu projeto. Para cada módulo importado, o Bun precisa encontrar e ler aquele módulo. A parte de "encontrar" é conhecida como "resolver" um módulo.
O callback de lifecycle onResolve() permite que você configure como um módulo é resolvido.
O primeiro argumento para onResolve() é um objeto com uma propriedade filter e namespace. O filter é uma expressão regular que é executada na string de import. Efetivamente, isso permite que você filtre quais módulos sua lógica de resolução customizada será aplicada.
O segundo argumento para onResolve() é um callback que é executado para cada import de módulo que o Bun encontra que corresponde ao filter e namespace definidos no primeiro argumento.
O callback recebe como input o caminho para o módulo correspondente. O callback pode retornar um novo caminho para o módulo. O Bun lerá o conteúdo do novo caminho e o parseará como um módulo.
Por exemplo, redirecionando todos os imports para images/ para ./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 },
defer: () => Promise<void>,
callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
): void;Após o bundler do Bun ter resolvido um módulo, ele precisa ler o conteúdo do módulo e parseá-lo.
O callback de lifecycle onLoad() permite que você modifique o conteúdo de um módulo antes que ele seja lido e parseado pelo Bun.
Como onResolve(), o primeiro argumento para onLoad() permite que você filtre quais módulos esta invocação de onLoad() será aplicada.
O segundo argumento para onLoad() é um callback que é executado para cada módulo correspondente antes que o Bun carregue o conteúdo do módulo na memória.
Este callback recebe como input o caminho para o módulo correspondente, o importer do módulo (o módulo que importou o módulo), o namespace do módulo e o kind do módulo.
O callback pode retornar uma nova string contents para o módulo assim como um novo loader.
Por exemplo:
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"Este plugin transformará todos os imports da forma import env from "env" em um módulo JavaScript que exporta as variáveis de ambiente atuais.
.defer()
Um dos argumentos passados para o callback onLoad é uma função defer. Esta função retorna uma Promise que é resolvida quando todos os outros módulos foram carregados.
Isso permite que você atrase a execução do callback onLoad até que todos os outros módulos tenham sido carregados.
Isso é útil para retornar conteúdo de um módulo que depende de outros módulos.
Exemplo: rastrear e reportar exports não utilizados
import { plugin } from "bun";
plugin({
name: "track imports",
setup(build) {
const transpiler = new Bun.Transpiler();
let trackedImports: Record<string, number> = {};
// Cada módulo que passa por este callback onLoad
// registrará seus imports em `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 }) => {
// Espera todos os arquivos serem carregados, garantindo
// que todo arquivo passe pela função `onLoad()` acima
// e seus imports sejam rastreados
await defer();
// Emite JSON contendo as estatísticas de cada import
return {
contents: `export default ${JSON.stringify(trackedImports)}`,
loader: "json",
};
});
},
});Native plugins
Uma das razões pelas quais o bundler do Bun é tão rápido é que ele é escrito em código nativo e aproveita multi-threading para carregar e parsear módulos em paralelo.
No entanto, uma limitação de plugins escritos em JavaScript é que o próprio JavaScript é single-threaded.
Native plugins são escritos como módulos NAPI e podem ser executados em múltiplas threads. Isso permite que native plugins rodem muito mais rápido que plugins JavaScript.
Além disso, native plugins podem pular trabalho desnecessário como a conversão UTF-8 -> UTF-16 necessária para passar strings para JavaScript.
Estes são os seguintes lifecycle hooks disponíveis para native plugins:
onBeforeParse(): Chamado em qualquer thread antes de um arquivo ser parseado pelo bundler do Bun.
Native plugins são módulos NAPI que expõem lifecycle hooks como funções C ABI.
Para criar um native plugin, você deve exportar uma função C ABI que corresponde à assinatura do lifecycle hook nativo que você quer implementar.
Criando um native plugin em Rust
Native plugins são módulos NAPI que expõem lifecycle hooks como funções C ABI.
Para criar um native plugin, você deve exportar uma função C ABI que corresponde à assinatura do lifecycle hook nativo que você quer implementar.
bun add -g @napi-rs/cli
napi newEntão instale este crate:
cargo add bun-native-pluginAgora, dentro do arquivo lib.rs, usaremos a proc macro bun_native_plugin::bun para definir uma função que implementará nosso native plugin.
Aqui está um exemplo implementando o hook onBeforeParse:
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;
/// Define o plugin e seu nome
define_bun_plugin!("replace-foo-with-bar");
/// Aqui implementaremos `onBeforeParse` com código que substitui todas as ocorrências de
/// `foo` com `bar`.
///
/// Usamos a macro #[bun] para gerar parte do código boilerplate.
///
/// O argumento da função (`handle: &mut OnBeforeParse`) diz
/// ao macro que esta função implementa o hook `onBeforeParse`.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
// Busca o código fonte de input.
let input_source_code = handle.input_source_code()?;
// Pega o Loader para o arquivo
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(())
}E para usá-lo em 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;Este callback de lifecycle é executado imediatamente antes de um arquivo ser parseado pelo bundler do Bun.
Como input, ele recebe o conteúdo do arquivo e pode opcionalmente retornar novo código fonte.