Bun

插件

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

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

用法

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

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 loader

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 内部也使用这些相同的加载器来处理各种类型的文件。下表是一个快速参考;有关完整文档,请参阅 打包器 > 加载器

加载器扩展名输出
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 提供,并且尚不支持打包器,但你可以使用 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 读取和写入 构建配置

await 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}`);
      },
    },
  ],
});

注意:插件生命周期回调 (onStart()onResolve() 等) 在 setup() 函数中无法修改 build.config 对象。 如果你想修改 build.config,你必须直接在 setup() 函数中进行操作

await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "demo",
      setup(build) {
        // ✅ good! modifying it directly in the setup() function
        build.config.minify = true;

        build.onStart(() => {
          // 🚫 uh-oh! this won't work!
          build.config.minify = false;
        });
      },
    },
  ],
});

生命周期钩子

插件可以注册回调,以便在 bundle 生命周期的各个点运行

参考

类型的大致概述 (完整类型定义请参考 Bun 的 bun.d.ts)

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

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 },
    callback: (args: { path: string }) => {
      loader?: Loader;
      contents?: string;
      exports?: Record<string, any>;
    },
  ) => void;
  config: BuildConfig;
};

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

命名空间

onLoadonResolve 接受一个可选的 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;

注册一个回调,以便在打包器启动新 bundle 时运行。

import { plugin } from "bun";

plugin({
  name: "onStart example",

  setup(build) {
    build.onStart(() => {
      console.log("Bundle started!");
    });
  },
});

回调可以返回一个 Promise。在 bundle 进程初始化后,打包器会等待所有 onStart() 回调完成,然后继续。

例如

const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "Sleep for 10 seconds",
      setup(build) {
        build.onStart(async () => {
          await Bunlog.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`;
        });
      },
    },
  ],
});

在上面的示例中,Bun 将等待第一个 onStart() (休眠 10 秒) 完成,以及 第二个 onStart() (将 bundle 时间写入文件)。

请注意,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() 的第一个参数是一个对象,该对象具有 filternamespace 属性。 filter 是一个正则表达式,它在导入字符串上运行。实际上,这些允许你过滤你的自定义解析逻辑将应用于哪些模块。

onResolve() 的第二个参数是一个回调,对于 Bun 找到的每个与第一个参数中定义的 filternamespace 匹配的模块导入,都会运行该回调。

回调接收匹配模块的路径作为输入。回调可以返回模块的新路径。Bun 将读取新路径的内容并将其解析为模块。

例如,将所有对 images/ 的导入重定向到 ./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 },
  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";

plugin({
  name: "env plugin",
  setup(build) {
    build.onLoad({ filter: /env/, namespace: "file" }, args => {
      return {
        contents: `export default ${JSON.stringify(process.env)}`,
        loader: "js",
      };
    });
  },
});

此插件将把所有 import env from "env" 形式的导入转换为导出当前环境变量的 JavaScript 模块。