Skip to content

Bun 은 런타임번들러 모두를 확장하는 데 사용할 수 있는 범용 플러그인 API 를 제공합니다.

플러그인은 import 를 인터셉트하고 파일 읽기, 코드 트랜스파일 등 사용자 정의 로딩 로직을 수행합니다. .scss.yaml 와 같은 추가 파일 타입을 지원하는 데 사용할 수 있습니다. Bun 번들러의 맥락에서 플러그인은 CSS 추출, 매크로, 클라이언트 - 서버 코드 공동 배치와 같은 프레임워크 수준의 기능을 구현하는 데 사용할 수 있습니다.

라이프사이클 훅

플러그인은 번들 라이프사이클의 다양한 시점에 실행되는 콜백을 등록할 수 있습니다:

  • onStart(): 번들러가 번들을 시작하면 실행
  • onResolve(): 모듈이 해결되기 전에 실행
  • onLoad(): 모듈이 로드되기 전에 실행
  • onBeforeParse(): 파일이 파싱되기 전에 파서 스레드에서 제로 - 카피 네이티브 애드온 실행

참조

타입의 대략적인 개요 (전체 타입 정의는 Bun 의 bun.d.ts 참조):

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 객체로 정의됩니다.

tsx
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "Custom loader",
  setup(build) {
    // 구현
  },
};

이 플러그인은 Bun.build 를 호출할 때 plugins 배열에 전달할 수 있습니다.

ts
await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./out",
  plugins: [myPlugin],
});

플러그인 라이프사이클

네임스페이스

onLoadonResolve 는 선택적 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

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

번들러가 새 번들을 시작할 때 실행될 콜백을 등록합니다.

ts
import { plugin } from "bun";

plugin({
  name: "onStart 예제",

  setup(build) {
    build.onStart(() => {
      console.log("번들 시작!");
    });
  },
});

콜백은 Promise 를 반환할 수 있습니다. 번들 프로세스가 초기화된 후, 번들러는 모든 onStart() 콜백이 완료될 때까지 기다렸다가 계속합니다.

예를 들어:

ts
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

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

프로젝트를 번들링하기 위해 Bun 은 프로젝트의 모든 모듈 의존성 트리를 따라 내려갑니다. 각 import 된 모듈에 대해 Bun 은 실제로 해당 모듈을 찾아야 합니다. "찾기" 부분을 "모듈 해결"이라고 합니다.

onResolve() 플러그인 라이프사이클 콜백은 모듈이 어떻게 해결되는지 구성할 수 있게 해줍니다.

onResolve() 의 첫 번째 인수는 filternamespace 속성을 가진 객체입니다. 필터는 import 문자열에 대해 실행되는 정규식입니다. 효과적으로 사용자 정의 해결 로직이 적용될 모듈을 필터링할 수 있습니다.

onResolve() 의 두 번째 인수는 filternamespace 와 일치하는 각 모듈 import 에 대해 실행되는 콜백입니다.

콜백은 일치하는 모듈의 경로 를 입력으로 받습니다. 콜백은 모듈에 대한 새 경로 를 반환할 수 있습니다. Bun 은 새 경로 의 내용을 읽고 모듈로 파싱합니다.

예를 들어, images/ 에 대한 모든 import 를 ./public/images/ 로 리디렉션:

ts
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

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 이 모듈의 내용을 메모리로 로드하기 전에 각 일치하는 모듈에 대해 실행되는 콜백입니다.

이 콜백은 일치하는 모듈의 경로, 모듈을 import 한 importer, 모듈의 네임스페이스, 모듈의 kind 를 입력으로 받습니다.

콜백은 모듈에 대한 새 contents 문자열과 새 loader 를 반환할 수 있습니다.

예를 들어:

ts
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 추적 및 보고
ts
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 함수를 내보내야 합니다.

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");

/// 여기서는 `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() 에서 사용하는 방법:

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 by www.bunjs.com.cn 편집