Plugin System

Rsbuild provides a lightweight yet powerful plugin system to implement most of its functionality and allows for user extension.

Plugins written by developers can modify the default behavior of Rsbuild and add various additional features, including but not limited to:

  • Get context information
  • Register lifecycle hooks
  • Transform module source code
  • Modify Rspack configuration
  • Modify Rsbuild configuration
  • ...

Comparison

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

Generally, Rsbuild's plugin API is similar to esbuild, and compared with Webpack / Rspack plugins, Rsbuild's plugin API is more simple 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 and provides some hooks for extension. On the other hand, Rspack's plugin API is more complex and rich, 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 uses lifetime planning work internally, 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.

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

Use Rsbuild Config

Custom plugins can usually get config from function parameters, just define and use it at your pleasure.

But sometimes you may need to read and change the public config of the Rsbuild. To begin with, you should understand how the Rsbuild generates and uses its config:

  • Read, parse config and merge with default values.
  • Plugins modify the config by api.modifyRsbuildConfig(...).
  • Normalize the config and provide it to consume, then the config can no longer be modified.

Refer to this tiny example:

export const pluginUploadDist = (): RsbuildPlugin => ({
  name: 'plugin-upload-dist',
  setup(api) {
    api.modifyRsbuildConfig((config) => {
      // try to disable minimize.
      config.output ||= {};
      config.output.minify = false;
      // but also can be enable by other plugins...
    });
    api.onBeforeBuild(() => {
      // use the normalized config.
      const config = api.getNormalizedConfig();
      if (config.output.minify !== false) {
        // let it crash when enable minimize.
        throw new Error(
          'You must disable minimize to upload readable dist files.',
        );
      }
    });
    api.onAfterBuild(() => {
      const config = api.getNormalizedConfig();
      const distRoot = config.output.distPath.root;

      // upload all files in `distRoot`...
    });
  },
});

There are 3 ways to use Rsbuild config:

  • register callback with api.modifyRsbuildConfig(config => {}) to modify config.
  • use api.getRsbuildConfig() to get Rsbuild config.
  • use api.getNormalizedConfig() to get finally normalized config.

When normalized, it will again merge the config object with the default values and make sure the optional properties exist. So for PluginUploadDist, part of its type looks like:

api.modifyRsbuildConfig((config: RsbuildConfig) => {});
api.getRsbuildConfig() as RsbuildConfig;
type RsbuildConfig = {
  output?: {
    minify?: boolean;
    distPath?: { root?: string };
  };
};

api.getNormalizedConfig() as NormalizedConfig;
type NormalizedConfig = {
  output: {
    minify: boolean;
    distPath: { root: string };
  };
};

The return value type of getNormalizedConfig() is slightly different from that of RsbuildConfig and is narrowed compared to the types described elsewhere in the documentation. You don't need to fill in the defaults when you use it.

Therefore, the best way to use configuration options is to

  • Modify the config with api.modifyRsbuildConfig(config => {})
  • Read api.getNormalizedConfig() as the actual config used by the plugin in the further lifetime.

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