Introduce to Plugin

Builder provides developers with a lightweight but powerful plugin system to build itself and any other plugins. Developing plugins to change the Builder's behavior and introduce extra features. such as:

  • Modify config of bundlers.
  • Resolve and handle new file types.
  • Modify and compile file modules.
  • Deploy your application.

Builder can use webpack or Rspack as the bundler, expose unified Node.js API, and integrate into different frameworks. Users can painlessly switch between bundlers.

Write a plugin

Plugin module should export an entry function just like (options?: PluginOptions) => BuilderPlugin, It is recommended to name plugin functions builderPluginXXX.

import type { BuilderPlugin } from '@modern-js/builder-webpack-provider';

export interface PluginFooOptions {
  message?: string;
}

export const builderPluginFoo = (
  options?: PluginFooOptions,
): BuilderPlugin => ({
  name: 'plugin-foo',
  setup(api) {
    api.onExit(() => {
      const msg = options.message || 'finish.';
      console.log(msg);
    });
  },
});

builder.addPlugins([builderPluginFoo('some other message.')]);

The function usually takes an options object and returns the plugin instance, which manages state through closures.

Let's look at each part:

  • name is used to label the plugin
  • setup as the main entry point of plugins
  • api provides context object, lifetime hooks and utils.

The package name of the plugin needs to contain the conventional builder-plugin prefix for identification, just like builder-plugin-foo, @scope/builder-plugin-bar, etc.

Lifetime Hooks

Builder 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 Builder's lifetime hooks can be found in the API References.

The Builder does not take over the hooks of the underlying bundlers, whose documents can be found here: webpack hooks

Use Builder 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 Builder. To begin with, you should understand how the Builder generates and uses its config:

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

Refer to this tiny example:

export const builderPluginUploadDist = (): BuilderPlugin => ({
  name: 'plugin-upload-dist',
  setup(api) {
    api.modifyBuilderConfig(config => {
      // try to disable minimize.
      // should deal with optional value by self.
      config.output ||= {};
      config.output.disableMinimize = true;
      // but also can be enable by other plugins...
    });
    api.onBeforeBuild(() => {
      // use the normalized config.
      const config = api.getNormalizedConfig();
      if (!config.output.disableMinimize) {
        // 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;
      // TODO: upload all files in `distRoot`.
    });
  },
});

There are 3 ways to use Builder config:

  • register callback with api.modifyBuilderConfig(config => {}) to modify config.
  • use api.getBuilderConfig() to get Builder 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.modifyBuilderConfig((config: BuilderConfig) => {});
api.getBuilderConfig() as BuilderConfig;
type BuilderConfig = {
  output?: {
    disableMinimize?: boolean;
    distPath?: { root?: string };
  };
};

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

The return value type of getNormalizedConfig() is slightly different from that of BuilderConfig 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.modifyBuilderConfig(config => {})
  • Read api.getNormalizedConfig() as the actual config used by the plugin in the further lifetime.

Modify webpack Config

Plugins can handle webpack's config by:

  • use api.modifyWebpackChain(chain => {}) to modify webpack-chain.
  • use api.modifyWebpackConfig(config => {}) to modify raw webpack config.
  • use api.onAfterCreateCompiler(compiler => {}) to handle webpack instance directly.

We recommend that you use neutrinojs/webpack-chain's chained api to handle the config of webpack whenever possible.

Builder integrated a webpack5-compatible version, which can be found in sorrycc/webpack-chain.

Examples

Modify Loaders

The webpack loaders can be used to load and transform various file types. For more information, see concepts and loaders.

import type { BuilderPlugin } from '@modern-js/builder-webpack-provider';

export const builderPluginTypeScriptExt = (): BuilderPlugin => ({
  name: 'builder-typescript-ext',
  setup(api) {
    api.modifyWebpackChain(async chain => {
      // set ts-loader to recognize more files as typescript modules.
      chain.module.rule(CHAIN_ID.RULE.TS).test(/\.(ts|mts|cts|tsx|tss|tsm)$/);
    });
  },
});

Add Entry Points

import type { BuilderPlugin } from '@modern-js/builder-webpack-provider';

export const builderPluginAdminPanel = (): BuilderPlugin => ({
  name: 'builder-admin-panel',
  setup(api) {
    api.modifyWebpackChain(async chain => {
      config.entry('admin-panel').add('src/admin/panel.js');
    });
  },
});

Integrate webpack Plugins

Integrate existing webpack plugins to migrate your applications:

import type { BuilderPlugin } from '@modern-js/builder-webpack-provider';
import type { Options } from '@modern-js/inspector-webpack-plugin';

export const builderPluginInspector = (options?: Options): BuilderPlugin => ({
  name: 'builder-plugin-inspector',
  setup(api) {
    api.modifyWebpackChain(async chain => {
      // load modules dynamically only when needed
      // to avoid unnecessary performance cost.
      const { InspectorWebpackPlugin } = await import(
        '@modern-js/inspector-webpack-plugin'
      );
      // modify webpack-chain to setup webpack plugin.
      chain
        .plugin('inspector-webpack-plugin')
        .use(InspectorWebpackPlugin, [inspectorOptions]);
    });
  },
});