Bun 은 런타임 과 번들러 모두를 확장하는 데 사용할 수 있는 범용 플러그인 API 를 제공합니다.
플러그인은 import 를 인터셉트하고 파일 읽기, 코드 트랜스파일 등 사용자 정의 로딩 로직을 수행합니다. .scss 나 .yaml 와 같은 추가 파일 타입을 지원하는 데 사용할 수 있습니다. Bun 번들러의 맥락에서 플러그인은 CSS 추출, 매크로, 클라이언트 - 서버 코드 공동 배치와 같은 프레임워크 수준의 기능을 구현하는 데 사용할 수 있습니다.
라이프사이클 훅
플러그인은 번들 라이프사이클의 다양한 시점에 실행되는 콜백을 등록할 수 있습니다:
onStart(): 번들러가 번들을 시작하면 실행onResolve(): 모듈이 해결되기 전에 실행onLoad(): 모듈이 로드되기 전에 실행onBeforeParse(): 파일이 파싱되기 전에 파서 스레드에서 제로 - 카피 네이티브 애드온 실행
참조
타입의 대략적인 개요 (전체 타입 정의는 Bun 의 bun.d.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";사용법
플러그인은 name 속성과 setup 함수를 포함하는 간단한 JavaScript 객체로 정의됩니다.
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Custom loader",
setup(build) {
// 구현
},
};이 플러그인은 Bun.build 를 호출할 때 plugins 배열에 전달할 수 있습니다.
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
plugins: [myPlugin],
});플러그인 라이프사이클
네임스페이스
onLoad 와 onResolve 는 선택적 namespace 문자열을 허용합니다. 네임스페이스란 무엇일까요?
모든 모듈에는 네임스페이스가 있습니다. 네임스페이스는 트랜스파일된 코드에서 import 앞에 접두사로 사용됩니다. 예를 들어 filter: /\.yaml$/ 및 namespace: "yaml:" 이 있는 로더는 ./myfile.yaml 에서의 import 를 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()(번들 시간을 파일에 기록) 가 완료될 때까지 기다립니다.
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 은 프로젝트의 모든 모듈 의존성 트리를 따라 내려갑니다. 각 import 된 모듈에 대해 Bun 은 실제로 해당 모듈을 찾아야 합니다. "찾기" 부분을 "모듈 해결"이라고 합니다.
onResolve() 플러그인 라이프사이클 콜백은 모듈이 어떻게 해결되는지 구성할 수 있게 해줍니다.
onResolve() 의 첫 번째 인수는 filter 와 namespace 속성을 가진 객체입니다. 필터는 import 문자열에 대해 실행되는 정규식입니다. 효과적으로 사용자 정의 해결 로직이 적용될 모듈을 필터링할 수 있습니다.
onResolve() 의 두 번째 인수는 filter 와 namespace 와 일치하는 각 모듈 import 에 대해 실행되는 콜백입니다.
콜백은 일치하는 모듈의 경로 를 입력으로 받습니다. 콜백은 모듈에 대한 새 경로 를 반환할 수 있습니다. Bun 은 새 경로 의 내용을 읽고 모듈로 파싱합니다.
예를 들어, images/ 에 대한 모든 import 를 ./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 이 모듈의 내용을 메모리로 로드하기 전에 각 일치하는 모듈에 대해 실행되는 콜백입니다.
이 콜백은 일치하는 모듈의 경로, 모듈을 import 한 importer, 모듈의 네임스페이스, 모듈의 kind 를 입력으로 받습니다.
콜백은 모듈에 대한 새 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" 형태의 모든 import 를 현재 환경 변수를 내보내는 JavaScript 모듈로 변환합니다.
.defer()
onLoad 콜백에 전달되는 인수 중 하나는 defer 함수입니다. 이 함수는 다른 모든 모듈이 로드되었을 때 해결되는 Promise 를 반환합니다.
이를 통해 다른 모든 모듈이 로드될 때까지 onLoad 콜백의 실행을 지연할 수 있습니다.
이는 다른 모듈에 의존하는 모듈의 내용을 반환할 때 유용합니다.
예제: 사용되지 않는 export 추적 및 보고
import { plugin } from "bun";
plugin({
name: "import 추적",
setup(build) {
const transpiler = new Bun.Transpiler();
let trackedImports: Record<string, number> = {};
// 이 onLoad 콜백을 거치는 각 모듈은
// `trackedImports` 에 import 를 기록합니다
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()` 함수를 거치고
// import 가 추적되도록 합니다
await defer();
// 각 import 의 통계를 포함하는 JSON 을 내보냅니다
return {
contents: `export default ${JSON.stringify(trackedImports)}`,
loader: "json",
};
});
},
});.defer() 함수는 현재 각 onLoad 콜백당 한 번만 호출할 수 있다는 제한이 있습니다.
네이티브 플러그인
Bun 의 번들러가 매우 빠른 이유 중 하나는 네이티브 코드로 작성되었으며 멀티스레딩을 활용하여 모듈을 병렬로 로드하고 파싱하기 때문입니다.
그러나 JavaScript 로 작성된 플러그인의 제한 사항 중 하나는 JavaScript 자체가 단일 스레드라는 것입니다.
네이티브 플러그인은 NAPI 모듈로 작성되며 여러 스레드에서 실행될 수 있습니다. 이를 통해 네이티브 플러그인은 JavaScript 플러그인보다 훨씬 빠르게 실행될 수 있습니다.
또한 네이티브 플러그인은 JavaScript 에 문자열을 전달하는 데 필요한 UTF-8 -> UTF-16 변환과 같은 불필요한 작업을 건너뛸 수 있습니다.
네이티브 플러그인에서 사용할 수 있는 라이프사이클 훅은 다음과 같습니다:
onBeforeParse(): Bun 의 번들러가 파일을 파싱하기 전에 스레드에서 호출됨
네이티브 플러그인은 라이프사이클 훅을 C ABI 함수로 내보내는 NAPI 모듈입니다.
네이티브 플러그인을 만들려면 구현하려는 네이티브 라이프사이클 훅의 시그니처와 일치하는 C ABI 함수를 내보내야 합니다.
Rust 에서 네이티브 플러그인 만들기
네이티브 플러그인은 라이프사이클 훅을 C ABI 함수로 내보내는 NAPI 모듈입니다.
네이티브 플러그인을 만들려면 구현하려는 네이티브 라이프사이클 훅의 시그니처와 일치하는 C ABI 함수를 내보내야 합니다.
bun add -g @napi-rs/cli
napi new그런 다음 이 크레이트를 설치합니다:
cargo add bun-native-plugin이제 lib.rs 파일 내에서 bun_native_plugin::bun proc 매크로를 사용하여 네이티브 플러그인을 구현하는 함수를 정의합니다.
다음은 onBeforeParse 훅을 구현하는 예제입니다:
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;
/// 플러그인과 이름 정의
define_bun_plugin!("replace-foo-with-bar");
/// 여기서는 `foo` 를 `bar` 로 대체하는 코드로
/// `onBeforeParse` 를 구현합니다.
///
/// #[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 의 번들러가 파일을 파싱하기 직전에 실행됩니다.
입력으로 파일의 내용을 받으며 선택적으로 새 소스 코드를 반환할 수 있습니다.
이 콜백은 어떤 스레드에서든 호출될 수 있으므로 napi 모듈 구현은 스레드 안전해야 합니다.