Bun

插件

Bun 提供了一个通用的插件 API,可用于扩展运行时bundler

插件拦截导入并执行自定义加载逻辑:读取文件、转换代码等。它们可用于添加对其他文件类型(如 .scss.yaml)的支持。在 Bun 的 bundler 的上下文中,插件可用于实现框架级功能,如 CSS 提取、宏和客户端-服务器代码并置。

用法

插件被定义为包含 name 属性和 setup 函数的简单 JavaScript 对象。使用 plugin 函数向 Bun 注册插件。

myPlugin.ts
import { plugin, type BunPlugin } from "bun";

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

plugin(myPlugin);

插件必须在任何其他代码运行之前加载!要实现此目的,请在 bunfig.toml 中使用 preload 选项。Bun 会在运行文件之前自动加载 preload 中指定的文件/模块。

preload = ["./myPlugin.ts"]

要在 bun test 之前预加载文件

[test]
preload = ["./myPlugin.ts"]

第三方插件

根据惯例,供消费的第三方插件应导出一个工厂函数,该函数接受一些配置并返回一个插件对象。

import { plugin } from "bun";
import fooPlugin from "bun-plugin-foo";

plugin(
  fooPlugin({
    // configuration
  }),
);

Bun 的插件 API 松散基于 esbuild。仅实现了 esbuild API 的 子集,但一些 esbuild 插件在 Bun 中“直接可用”,例如官方 MDX 加载器

import { plugin } from "bun";
import mdx from "@mdx-js/esbuild";

plugin(mdx());

加载器

插件主要用于使用其他文件类型的加载器扩展 Bun。我们来看一个简单的插件,它为 .yaml 文件实现了加载器。

yamlPlugin.ts
import { plugin } from "bun";

await plugin({
  name: "YAML",
  async setup(build) {
    const { load } = await import("js-yaml");

    // when a .yaml file is imported...
    build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {

      // read and parse the file
      const text = await Bun.file(args.path).text();
      const exports = load(text) as Record<string, any>;

      // and returns it as a module
      return {
        exports,
        loader: "object", // special loader for JS objects
      };
    });
  },
});

preload 中注册此文件

bunfig.toml
preload = ["./yamlPlugin.ts"]

注册插件后,可以直接导入 .yaml.yml 文件。

index.ts
data.yml
index.ts
import data from "./data.yml"

console.log(data);
data.yml
name: Fast X
releaseYear: 2023

请注意,返回的对象具有 loader 属性。这会告诉 Bun 应使用其哪个内部加载器来处理结果。即使我们正在为 .yaml 实现加载器,结果仍必须是 Bun 的内置加载器之一可以理解的。它一直都是加载器。

在这种情况下,我们使用 "object"——一个内置加载器(供插件使用),它将纯 JavaScript 对象转换为等效的 ES 模块。支持 Bun 的任何内置加载器;Bun 在内部使用这些相同的加载器来处理各种文件。下表是一个快速参考;有关完整文档,请参阅 Bundler > 加载器

加载器扩展名输出
js.mjs .cjs编译为 JavaScript 文件
jsx.js .jsx转换 JSX,然后编译
ts.ts .mts .cts转换 TypeScript,然后编译
tsx.tsx转换 TypeScript、JSX,然后编译
toml.toml使用 Bun 的内置 TOML 解析器进行解析
json.json使用 Bun 的内置 JSON 解析器进行解析
napi.node导入一个原生 Node.js 插件
wasm.wasm导入一个原生 Node.js 插件
objectnone一个专门用于插件的加载器,它将一个普通的 JavaScript 对象转换为一个等效的 ES 模块。对象中的每个键对应一个具名导出。

加载一个 YAML 文件很有用,但插件支持的不仅仅是数据加载。我们来看一个允许 Bun 导入 *.svelte 文件的插件。

sveltePlugin.ts
import { plugin } from "bun";

await plugin({
  name: "svelte loader",
  async setup(build) {
    const { compile } = await import("svelte/compiler");

    // when a .svelte file is imported...
    build.onLoad({ filter: /\.svelte$/ }, async ({ path }) => {

      // read and compile it with the Svelte compiler
      const file = await Bun.file(path).text();
      const contents = compile(file, {
        filename: path,
        generate: "ssr",
      }).js.code;

      // and return the compiled source code as "js"
      return {
        contents,
        loader: "js",
      };
    });
  },
});

注意:在生产环境中,你可能希望缓存编译后的输出并包含额外的错误处理。

build.onLoad 返回的对象包含 contents 中的编译后的源代码,并指定 "js" 作为其加载器。这告诉 Bun 将返回的 contents 视为一个 JavaScript 模块,并使用 Bun 的内置 js 加载器对其进行转换。

有了这个插件,Svelte 组件现在可以直接导入和使用。

import "./sveltePlugin.ts";
import MySvelteComponent from "./component.svelte";

console.log(MySvelteComponent.render());

虚拟模块

此功能目前仅在运行时使用 Bun.plugin,并且还不受 bundler 支持,但你可以使用 onResolveonLoad 来模拟此行为。

要在运行时创建虚拟模块,请在 Bun.pluginsetup 函数中使用 builder.module(specifier, callback)

例如

import { plugin } from "bun";

plugin({
  name: "my-virtual-module",

  setup(build) {
    build.module(
      // The specifier, which can be any string - except a built-in, such as "buffer"
      "my-transpiled-virtual-module",
      // The callback to run when the module is imported or required for the first time
      () => {
        return {
          contents: "console.log('hello world!')",
          loader: "js",
        };
      },
    );

    build.module("my-object-virtual-module", () => {
      return {
        exports: {
          foo: "bar",
        },
        loader: "object",
      };
    });
  },
});

// Sometime later
// All of these work
import "my-transpiled-virtual-module";
require("my-transpiled-virtual-module");
await import("my-transpiled-virtual-module");
require.resolve("my-transpiled-virtual-module");

import { foo } from "my-object-virtual-module";
const object = require("my-object-virtual-module");
await import("my-object-virtual-module");
require.resolve("my-object-virtual-module");

覆盖现有模块

你还可以使用 build.module 覆盖现有模块。

import { plugin } from "bun";
build.module("my-object-virtual-module", () => {
  return {
    exports: {
      foo: "bar",
    },
    loader: "object",
  };
});

require("my-object-virtual-module"); // { foo: "bar" }
await import("my-object-virtual-module"); // { foo: "bar" }

build.module("my-object-virtual-module", () => {
  return {
    exports: {
      baz: "quix",
    },
    loader: "object",
  };
});
require("my-object-virtual-module"); // { baz: "quix" }
await import("my-object-virtual-module"); // { baz: "quix" }

读取配置

插件可以使用 build.config 读取和写入 构建配置

Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "demo",
      setup(build) {
        console.log(build.config.sourcemap); // "external"

        build.config.minify = true; // enable minification

        // `plugins` is readonly
        console.log(`Number of plugins: ${build.config.plugins.length}`);
      },
    },
  ],
});

参考

namespace Bun {
  function plugin(plugin: {
    name: string;
    setup: (build: PluginBuilder) => void;
  }): void;
}

type PluginBuilder = {
  onResolve: (
    args: { filter: RegExp; namespace?: string },
    callback: (args: { path: string; importer: string }) => {
      path: string;
      namespace?: string;
    } | void,
  ) => void;
  onLoad: (
    args: { filter: RegExp; namespace?: string },
    callback: (args: { path: string }) => {
      loader?: Loader;
      contents?: string;
      exports?: Record<string, any>;
    },
  ) => void;
  config: BuildConfig;
};

type Loader = "js" | "jsx" | "ts" | "tsx" | "json" | "toml" | "object";

除了 filter 正则表达式外,onLoad 方法还可以选择接受一个 namespace。此命名空间将用于在转换后的代码中为导入添加前缀;例如,一个 filter: /\.yaml$/namespace: "yaml:" 的加载器将把来自 ./myfile.yaml 的导入转换为 yaml:./myfile.yaml