close

Plugin development

Rsbuild's architecture is centered on a plugin system. Most of Rsbuild's functionality is implemented through plugins, which keeps the core lightweight while providing flexible extensibility.

Rsbuild plugins are functions that can register hooks at different stages, listen to events, and execute custom logic. If you want to modify the default behavior, add new features, or integrate third-party tools, plugins provide a comprehensive API to fulfill these requirements.

Comparison

Before developing a Rsbuild plugin, you may have been familiar with the plugin systems of tools such as webpack, Vite, esbuild, etc.

Rsbuild's plugin API is similar to esbuild's, and compared with webpack or Rspack plugins, Rsbuild's plugin API is simpler and easier to get started with.

// esbuild plugin
const esbuildPlugin = {
  name: 'example',
  setup(build) {
    build.onEnd(() => console.log('done'));
  },
};

// Rsbuild plugin
const rsbuildPlugin = () => ({
  name: 'example',
  setup(api) {
    api.onAfterBuild(() => console.log('done'));
  },
});

// Rspack plugin
class RspackExamplePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('RspackExamplePlugin', () => {
      console.log('done');
    });
  }
}

From a functional perspective, Rsbuild's plugin API mainly revolves around Rsbuild's operation process and build configuration, providing various hooks for extension. On the other hand, Rspack's plugin API is more complex and comprehensive, capable of modifying every aspect of the bundling process.

Rspack plugins can be integrated into Rsbuild plugins. If the hooks provided by Rsbuild do not meet your requirements, you can also implement the functionality using Rspack plugin and register Rspack plugins in the Rsbuild plugin:

const rsbuildPlugin = () => ({
  name: 'example',
  setup(api) {
    api.modifyRspackConfig((config) => {
      config.plugins.push(new RspackExamplePlugin());
    });
  },
});

Developing plugins

Plugins provide a function similar to (options?: PluginOptions) => RsbuildPlugin as an entry point.

Plugin example

pluginFoo.ts
import type { RsbuildPlugin } from '@rsbuild/core';

export type PluginFooOptions = {
  message?: string;
};

export const pluginFoo = (options: PluginFooOptions = {}): RsbuildPlugin => ({
  name: 'plugin-foo',

  setup(api) {
    api.onAfterStartDevServer(() => {
      const msg = options.message || 'hello!';
      console.log(msg);
    });
  },
});

Registering the plugin:

rsbuild.config.ts
import { pluginFoo } from './pluginFoo';

export default {
  plugins: [pluginFoo({ message: 'world!' })],
};

Plugin structure

Function-based plugins can accept an options object and return a plugin instance, managing internal state through closures.

The roles of each part are as follows:

  • The name property is used to label the plugin's name.
  • setup serves as the main entry point for the plugin logic.
  • The api object contains various hooks and utility functions.

Naming convention

The naming convention for plugins is as follows:

  • The function of the plugin is named pluginAbc and exported by name.
  • The name of the plugin follows the format scope:foo-bar or plugin-foo-bar, adding scope: can avoid naming conflicts with other plugins.

Here is an example:

pluginFooBar.ts
import type { RsbuildPlugin } from '@rsbuild/core';

export const pluginFooBar = (): RsbuildPlugin => ({
  name: 'scope:foo-bar',
  setup() {},
});
Tip

The name of official Rsbuild plugins uniformly uses rsbuild: as a prefix, for example, rsbuild:react corresponds to @rsbuild/plugin-react.

Template repository

rsbuild-plugin-template is a minimal Rsbuild plugin template repository that you can use as a basis for developing your Rsbuild plugin.

Environment plugin

Rsbuild supports building outputs for multiple environments at the same time, and supports add plugins for specified environment.

If you want the plugin you develop to support use as an Environment plugin, you need to pay attention to the following points:

  1. Each environment has its own Rsbuild config:
  2. Be aware of side effects, your plugin code may be executed multiple times:
    • When the same plugin is registered multiple times in different environments, it will be regarded as multiple Rsbuild plugins (even if they point to the same plugin instance), because they have different Rsbuild environment contexts.

Here is an example:

pluginFoo.ts
import type { RsbuildPlugin } from '@rsbuild/core';

export type PluginFooOptions = {
  title?: string;
};

export const pluginFoo = (options: PluginFooOptions = {}): RsbuildPlugin => ({
  name: 'plugin-foo',

  setup(api) {
    api.modifyEnvironmentConfig((config) => {
      config.html.title = options.title || 'My Default Title';
    });
    api.modifyBundlerChain((chain, { environment }) => {
      chain.name(environment.config.html.title);
    });
  },
});

Reference other plugins

Rsbuild's plugins config supports passing a nested array, which means you can reference and register other Rsbuild plugins within your plugin.

For example, register pluginBar within pluginFoo:

import { pluginBar } from 'rsbuild-plugin-bar';

export const pluginFoo = (): RsbuildPlugin => {
  const foo = {
    name: 'plugin-foo',
    setup(api) {
      // ...
    },
  };
  return [foo, pluginBar()];
};

Lifetime hooks

Rsbuild internally uses lifecycle hooks to schedule tasks, and plugins can also register hooks to take part in any stage of the workflow and implement their own features.

The full list of Rsbuild's lifetime hooks can be found in the API References.

Rsbuild does not take over the hooks of the underlying Rspack, whose documents can be found here: Rspack Plugin API.

Migrate Vite plugin

See Migrate Vite plugin to learn how to migrate a Vite plugin to Rsbuild plugin.

Read and modify Rsbuild config

When a plugin needs to read or modify the project's Rsbuild config, use Rsbuild's config APIs.

Modify the base config

Register api.modifyRsbuildConfig during setup to modify the base config before it is merged with each environment's config:

api.modifyRsbuildConfig((config) => {
  config.output.minify = false;
});

modifyRsbuildConfig is a global hook. If a change applies only to certain environments or depends on the current environment, use api.modifyEnvironmentConfig instead. See Global hooks vs environment hooks for details.

Read the normalized config

After the config modification hooks have run, call api.getNormalizedConfig without arguments to get the complete normalized config for all environments. It includes default values, so its type is narrower than the value returned by api.getRsbuildConfig.

api.onBeforeBuild(() => {
  const config = api.getNormalizedConfig();
  console.log(Object.keys(config.environments));
});

If no environment context is available but you need the config for one environment, pass its name to getNormalizedConfig:

api.onBeforeBuild(() => {
  const config = api.getNormalizedConfig({ environment: 'web' });
  console.log(config.output.target);
});

See NormalizedConfig and NormalizedEnvironmentConfig for their return types.

Read the current environment config

When a hook callback includes an environment context, prefer environment.config. It is the normalized result of merging the base config with the current environment's config.

api.onBeforeEnvironmentCompile(({ environment }) => {
  const { name, config } = environment;
  console.log(`${name}: ${config.output.target}`);
});

Read all environment configs

Global hooks such as onBeforeBuild and onAfterBuild receive environments, which contains the context for every environment. Iterate over it when a plugin needs to read each environment's config:

api.onBeforeBuild(({ environments }) => {
  for (const { name, config } of Object.values(environments)) {
    console.log(`${name}: ${config.output.distPath.root}`);
  }
});

See Multi-environment builds for more details about environment configs.

Modify Rspack configuration

Rsbuild plugin allows you to modify the built-in Rspack configuration, including:

Example

For example, register eslint-rspack-plugin via Rsbuild plugin:

import type { RsbuildPlugin } from '@rsbuild/core';
import ESLintRspackPlugin from 'eslint-rspack-plugin';

export const pluginEslint = (options?: Options): RsbuildPlugin => ({
  name: 'plugin-eslint',
  setup(api) {
    api.modifyRspackConfig((config) => {
      config.plugins.push(
        new ESLintRspackPlugin({
          // plugins options
        }),
      );
    });
  },
});

Extending plugin API

When building custom tools on top of Rsbuild's JavaScript API, you may want to extend the existing plugin API to provide additional capabilities — for example, exposing utility functions or sharing context objects.

You can achieve this by using the rsbuild.expose() method on the Rsbuild instance.

This method works the same way as the plugin's api.expose(), allowing you to expose custom methods or objects to Rsbuild plugins.

For example, you can expose getState and setCount methods:

myToolkit.ts
import { createRsbuild } from '@rsbuild/core';

export const MY_TOOLKIT_ID = 'my-toolkit';

const rsbuild = await createRsbuild({
  // ...
});

const state = {
  count: 0,
};

rsbuild.expose(MY_TOOLKIT_ID, {
  getState() {
    return state;
  },
  setCount(count: number) {
    state.count = count;
  },
});

Plugins can then access these extended APIs using the api.useExposed() method:

myPlugin.ts
import { MY_TOOLKIT_ID } from './myToolkit';

const myPlugin = {
  name: 'my-plugin',
  setup(api) {
    const toolkitApi = api.useExposed(MY_TOOLKIT_ID);
    if (toolkitApi) {
      const { count } = toolkitApi.getState();
      toolkitApi.setCount(count + 1);
    }
  },
};

Dependency declaration

When publishing an Rsbuild plugin, you should declare @rsbuild/core in peerDependencies in package.json, and install it in devDependencies for local development:

{
  "peerDependencies": {
    "@rsbuild/core": "^2.0.0"
  },
  "devDependencies": {
    "@rsbuild/core": "^2.0.0"
  }
}

If your plugin only references types from @rsbuild/core, you can declare it as an optional peer dependency:

{
  "peerDependencies": {
    "@rsbuild/core": "^2.0.0"
  },
  "peerDependenciesMeta": {
    "@rsbuild/core": {
      "optional": true
    }
  }
}

In this case, the plugin will not produce unnecessary peer dependency warnings when it is used by higher-level tools based on Rsbuild, such as Rslib or Rspress.