diff --git a/packages/core/src/createRsbuild.ts b/packages/core/src/createRsbuild.ts index d3b97efadd..650e40c196 100644 --- a/packages/core/src/createRsbuild.ts +++ b/packages/core/src/createRsbuild.ts @@ -1,3 +1,4 @@ +import { isPromise } from 'node:util/types'; import { createContext } from './createContext'; import { getNodeEnv, pick, setNodeEnv } from './helpers'; import { initPluginAPI } from './initPlugins'; @@ -9,11 +10,14 @@ import type { Build, CreateDevServer, CreateRsbuildOptions, + Falsy, InternalContext, PluginManager, PreviewServerOptions, ResolvedCreateRsbuildOptions, RsbuildInstance, + RsbuildPlugin, + RsbuildPlugins, RsbuildProvider, StartDevServer, } from './types'; @@ -199,8 +203,19 @@ export async function createRsbuild( ]), }; + const getFlattenedPlugins = async (pluginOptions: RsbuildPlugins) => { + let plugins = pluginOptions; + do { + plugins = (await Promise.all(plugins)).flat( + Number.POSITIVE_INFINITY as 1, + ); + } while (plugins.some((v) => isPromise(v))); + + return plugins as Array; + }; + if (rsbuildConfig.plugins) { - const plugins = await Promise.all(rsbuildConfig.plugins); + const plugins = await getFlattenedPlugins(rsbuildConfig.plugins); rsbuild.addPlugins(plugins); } @@ -212,7 +227,7 @@ export async function createRsbuild( !rsbuildOptions.environment || rsbuildOptions.environment.includes(name); if (config.plugins && isEnvironmentEnabled) { - const plugins = await Promise.all(config.plugins); + const plugins = await getFlattenedPlugins(config.plugins); rsbuild.addPlugins(plugins, { environment: name, }); diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts index 2c887b8086..fd39940424 100644 --- a/packages/core/src/types/plugin.ts +++ b/packages/core/src/types/plugin.ts @@ -201,7 +201,8 @@ type LooseRsbuildPlugin = Omit & { export type RsbuildPlugins = ( | LooseRsbuildPlugin | Falsy - | Promise + | Promise + | RsbuildPlugins )[]; export type GetRsbuildConfig = { diff --git a/packages/core/tests/builder.test.ts b/packages/core/tests/builder.test.ts index ed25a6c49f..673be11072 100644 --- a/packages/core/tests/builder.test.ts +++ b/packages/core/tests/builder.test.ts @@ -23,3 +23,44 @@ describe('should use rspack as default bundler', () => { process.env.NODE_ENV = NODE_ENV; }); }); + +describe('plugins', () => { + it('should apply nested plugins correctly', async () => { + function myPlugin() { + return [ + { + name: 'plugin-foo', + setup() {}, + }, + { + name: 'plugin-bar', + setup() {}, + }, + ]; + } + + const rsbuild = await createRsbuild({ + rsbuildConfig: { + source: { + entry: { + index: './src/index.js', + }, + }, + plugins: [ + myPlugin(), + Promise.resolve([ + { + name: 'plugin-zoo', + setup() {}, + }, + ]), + ], + }, + }); + + expect(rsbuild.isPluginExists('plugin-foo')).toBeTruthy(); + expect(rsbuild.isPluginExists('plugin-bar')).toBeTruthy(); + expect(rsbuild.isPluginExists('plugin-zoo')).toBeTruthy(); + expect(rsbuild.isPluginExists('plugin-404')).toBeFalsy(); + }); +}); diff --git a/scripts/test-helper/src/rsbuild.ts b/scripts/test-helper/src/rsbuild.ts index 6331fb33c7..b4e49a72f8 100644 --- a/scripts/test-helper/src/rsbuild.ts +++ b/scripts/test-helper/src/rsbuild.ts @@ -1,7 +1,9 @@ +import { isPromise } from 'node:util/types'; import type { BundlerPluginInstance, CreateRsbuildOptions, RsbuildInstance, + RsbuildPlugin, RsbuildPlugins, Rspack, } from '@rsbuild/core'; @@ -53,10 +55,21 @@ export async function createStubRsbuild({ const rsbuild = await createRsbuild(rsbuildOptions); + const getFlattenedPlugins = async (pluginOptions: RsbuildPlugins) => { + let plugins = pluginOptions; + do { + plugins = (await Promise.all(plugins)).flat( + Number.POSITIVE_INFINITY as 1, + ); + } while (plugins.some((v) => isPromise(v))); + + return plugins as Array; + }; + if (plugins) { // remove all builtin plugins rsbuild.removePlugins(rsbuild.getPlugins().map((item) => item.name)); - rsbuild.addPlugins(await Promise.all(plugins)); + rsbuild.addPlugins(await getFlattenedPlugins(plugins)); } const unwrapConfig = async (index = 0) => { diff --git a/website/docs/en/config/plugins.mdx b/website/docs/en/config/plugins.mdx index 6156a6ac50..8d09e5bd04 100644 --- a/website/docs/en/config/plugins.mdx +++ b/website/docs/en/config/plugins.mdx @@ -12,7 +12,8 @@ type Falsy = false | null | undefined; type RsbuildPlugins = ( | RsbuildPlugin | Falsy - | Promise + | Promise + | RsbuildPlugins )[]; ``` @@ -47,6 +48,23 @@ By default, plugins are executed in the order they appear in the `plugins` array When a plugin internally uses fields that control the order, such as `pre` and `post`, the execution order is adjusted based on them. See [Pre Plugins](/plugins/dev/core#pre-pluginss) for more details. +## Nested Plugins + +Rsbuild also supports adding nested plugins. You can pass in an array containing multiple plugins, similar to a plugin preset collection. This is particularly useful for implementing complex functionalities that require a combination of multiple plugins (such as framework integration). + +```ts title="rsbuild.config.ts" +function myPlugin() { + return [ + fooPlugin(), + barPlugin() + ]; +} + +export default { + plugins: [myPlugin()] +} +``` + ## Local Plugins If your local code repository contains Rsbuild plugins, you can import them using relative paths. diff --git a/website/docs/zh/config/plugins.mdx b/website/docs/zh/config/plugins.mdx index 922dbabca4..0bb7f97d37 100644 --- a/website/docs/zh/config/plugins.mdx +++ b/website/docs/zh/config/plugins.mdx @@ -12,7 +12,8 @@ type Falsy = false | null | undefined; type RsbuildPlugins = ( | RsbuildPlugin | Falsy - | Promise + | Promise + | RsbuildPlugins )[]; ``` @@ -47,6 +48,23 @@ export default defineConfig({ 当插件内部使用了控制顺序的相关字段,比如 `pre`、`post` 时,执行顺序会基于它们进行调整,详见 [前置插件](/plugins/dev/core#前置插件)。 +## 嵌套插件 + +Rsbuild 还支持添加嵌套插件,你可以传入一个包含多个插件的数组,类似于一个插件预设集合,这对于实现需要多个插件组合的复杂功能(例如框架集成)很有帮助。 + +```ts title="rsbuild.config.ts" +function myPlugin() { + return [ + fooPlugin(), + barPlugin() + ]; +} + +export default { + plugins: [myPlugin()] +} +``` + ## 本地插件 如果本地代码仓库中包含了一些 Rsbuild 插件,你可以直接通过相对路径引入。