Develop Plugins

The previous section introduced the Hook models used by Modern.js plugins, while this section describes how to develop plugins.

Implementing a Plugin

A Modern.js plugin is an object that includes the following properties:

  • name: The name of the plugin, a unique identifier.
  • setup: The initialization function for the plugin, which only runs once. The setup function can return a Hooks object, which Modern.js executes at specific times.
const myPlugin = {
  name: 'my-plugin',

  setup() {
    const foo = '1';

    // return hook object
    return {
      afterBuild: () => {},
    };
  },
};

In addition, plugins allow configuration of the execution order with other plugins. For more information, please refer to Plugin Relationship.

Plugin Types

Modern.js supports various types of project development, such as application development (Modern.js Framework), module development (Modern.js Module), etc.

To balance the differences and commonalities between various types of project development, Modern.js organizes plugins as shown in the following figure:

plugin-relationship

As shown in the figure, Modern.js roughly divides plugins into two categories:

  1. Common plugins: Plugins that only include some basic Hooks.

  2. Project plugins: Different project developments will extend their own Hooks, Config, etc. on the basis of common plugins.

When using TypeScript, you can import built-in types such as CliPlugin to provide correct type inference for plugins.

import type { CliPlugin } from '@modern-js/core';

const myPlugin: CliPlugin = {
  name: 'my-plugin',

  setup() {
    const foo = '1';

    return {
      afterBuild: () => {},
    };
  },
};

The above code is a general-purpose plugin, containing only some basic Hooks. Modern.js supports extending the definition of plugins through generics:

import type { CliPlugin, AppTools } from '@modern-js/app-tools';

const myPlugin: CliPlugin<AppTools> = {
  name: 'my-plugin',

  setup() {
    const foo = '1';

    return {
      afterBuild: () => {},
    };
  },
};

If you look closely at the type AppTools, you can see that AppTools consists of 3 types.

type AppTools = {
  hooks: AppToolsHooks;
  userConfig: AppToolsUserConfig;
  normalizedConfig: AppToolsNormalizedConfig;
};

When writing plugins, plugins extend their own types like Hooks on different bases through generic extensions:

// common plugin
import type { CliPlugin } from '@modern-js/core';
import type { MyPluginHook } from 'xxx';

const myPlugin: CliPlugin<{ hooks: MyPluginHook }> = {};
// extend from app-tools hook
import type { CliPlugin, AppTools } from '@modern-js/app-tools';
import type { MyPluginHook } from 'xxx';

const myPlugin: CliPlugin<AppTools & { hooks: MyPluginHook }> = {};

Please refer to Extending Hooks for detailed explanations.

Plugin Configuration

It is recommended to develop plugins in the form of functions, so that plugins can receive configuration options through function parameters:

import type { CliPlugin } from '@modern-js/core';

type MyPluginOptions = {
  foo: string;
};

const myPlugin = (options: MyPluginOptions): CliPlugin => ({
  name: 'my-plugin',

  setup() {
    console.log(options.foo);
  },
});

Plugin API

The setup function of a plugin receives an api parameter, and you can call some methods provided on the api to get configuration, application context, and other information.

import type { CliPlugin } from '@modern-js/core';

export const myPlugin = (): CliPlugin => ({
  name: 'my-plugin',

  setup(api) {
    // get user set config
    const config = api.useConfigContext();
    // get context
    const appContext = api.useAppContext();
    // get final config
    const resolvedConfig = api.useResolvedConfigContext();
  },
});

For more detail Plugin API.

Async setup

The setup function of a CLI plugin can be an asynchronous function, which can execute asynchronous logic during the initialization process.

import type { CliPlugin } from '@modern-js/core';

export const myPlugin = (): CliPlugin => ({
  name: 'my-plugin',

  async setup(api) {
    await doSomething();
  },
});

Note that the setup function of the next plugin is not executed until the async setup function of the current plugin has finished. Therefore, you should avoid performing time-consuming asynchronous operations in the setup function to avoid slowing down the startup performance of the CLI.

Adding Plugins

Custom plugins can be used by following the instructions in the plugins section of the documentation. Below is the recommended way to implement plugins in Modern.js.

Developing Local Plugins

It is recommended to write local plugins in the config/plugin directory and export them using export default:

config/plugin/myPlugin.ts
import type { CliPlugin } from '@modern-js/core';

export const myPlugin = (): CliPlugin => ({
  name: 'my-plugin',

  setup() {
    // init plugin
  },
});

register plugin in modern.config.ts:

modern.config.ts
import { defineConfig } from '@modern-js/app-tools';
import { myPlugin } from './config/plugin/myPlugin';

export default defineConfig({
  plugins: [myPlugin()],
});

Publishing a Plugin on npm

If you want to publish your Modern.js plugin on npm, it's recommended to use the Modern.js Module to manage and build the plugin.

First, create an empty Modern.js Module project and adjust the package name:

{
  "name": "my-plugin"
  ...
}

Create plugin main file:

src/index.ts
import type { CliPlugin } from '@modern-js/core';

export const myPlugin = (): CliPlugin => ({
  name: 'my-plugin',

  setup() {
    // plugin init
  },
});

After publishing, install it to the project you need to use pnpm add my-plugin, take an application project as an example, and then add it in modern.config.ts:

modern.config.ts
import { defineConfig } from '@modern-js/app-tools';
import { myPlugin } from 'my-plugin';

export default defineConfig({
  plugins: [myPlugin()],
});

If you find that there are currently unsatisfactory scenarios in Modern.js, welcome to build the Modern.js ecosystem together by writing custom plugins.