Bun

插件

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

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

用法

插件定义为一个简单的 JavaScript 对象,其中包含一个 name 属性和一个 setup 函数。使用 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"]

预加载项可以是本地文件或 npm 包。任何可以被导入/require 的东西都可以被预加载。

preload = ["bun-plugin-foo"]

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 * as data from "./data.yml"

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

请注意,返回的对象有一个 loader 属性。这告诉 Bun 应该使用其哪个内部加载器来处理结果。即使我们正在为 .yaml 实现加载器,结果仍然必须被 Bun 的内置加载器之一理解。一切都由加载器决定。

在这种情况下,我们使用的是 "object" — 一个内置加载器(供插件使用),它将一个普通的 JavaScript 对象转换为等效的 ES 模块。对象中的每个键都对应一个命名导出。Bun 的任何内置加载器都支持;这些相同的加载器被 Bun 内部用于处理各种类型的文件。下表是一个快速参考;有关完整文档,请参阅 Bundler > Loaders

加载器扩展名输出
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;
        });
      },
    },
  ],
});

生命周期钩子

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

Reference

类型概览(请参阅 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;

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

import { plugin } from "bun";

plugin({
  name: "onStart example",

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

该回调可以返回一个 Promise。在包过程初始化后,打包器会等待所有 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()(将包时间写入文件)。

请注意,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 模块,该模块会导出当前的环境变量。