Bun 은 런타임과 번들러 모두를 확장하는 데 사용할 수 있는 범용 플러그인 API 를 제공합니다.
플러그인은 가져오기를 인터셉트하고 커스텀 로딩 로직을 수행합니다. 파일 읽기, 코드 트랜스파일 등. .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 문자열을 허용합니다. 네임스페이스란 무엇일까요?
모든 모듈에는 네임스페이스가 있습니다. 네임스페이스는 트랜스파일된 코드에서 가져오기에 접두사를 붙이는 데 사용됩니다. 예를 들어 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 은 새 경로의 내용을 읽고 모듈로 파싱합니다.
예를 들어, 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 플러그인보다 훨씬 빠르게 실행될 수 있습니다.
또한 네이티브 플러그인은 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 의 번들러에 의해 파일이 파싱되기 직전에 실행됩니다.
입력으로 파일의 내용을 받으며 선택적으로 새 소스 코드를 반환할 수 있습니다.