From 7b0c05291f0f6163662d6e10fab9e9f9a3a7bdf1 Mon Sep 17 00:00:00 2001 From: neverland Date: Wed, 31 Jan 2024 11:44:46 +0800 Subject: [PATCH] feat: support declare plugin hooks order (#1472) --- .../document/docs/en/plugins/dev/hooks.mdx | 60 ++++++++++++++++++- .../document/docs/zh/plugins/dev/hooks.mdx | 60 ++++++++++++++++++- packages/shared/src/createHook.ts | 32 ++++++++-- packages/shared/src/types/plugin.ts | 44 +++++++------- packages/shared/tests/createAsyncHook.test.ts | 49 +++++++++++++++ 5 files changed, 217 insertions(+), 28 deletions(-) diff --git a/packages/document/docs/en/plugins/dev/hooks.mdx b/packages/document/docs/en/plugins/dev/hooks.mdx index 76545b860e..953333d8bd 100644 --- a/packages/document/docs/en/plugins/dev/hooks.mdx +++ b/packages/document/docs/en/plugins/dev/hooks.mdx @@ -37,7 +37,7 @@ Called only when running the `rsbuild preview` command or the `rsbuild.preview() --- -## Execution Order +## Hooks Order ### Dev Hooks @@ -77,6 +77,64 @@ When executing the `rsbuild preview` command or `rsbuild.preview()` method, Rsbu --- +## Callback Order + +### Default Behavior + +If multiple plugins register the same hook, the callback functions of the hook will execute in the order in which they were registered. + +In the following example, the console will output `'1'` and `'2'` in sequence: + +```ts +const plugin1 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig(() => console.log('1')); + }, +}); + +const plugin2 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig(() => console.log('2')); + }, +}); + +rsbuild.addPlugins([plugin1, plugin2]); +``` + +### `order` Field + +When registering a hook, you can declare the order of hook through the `order` field. + +```ts +type HookDescriptor any> = { + handler: T; + order: 'pre' | 'post' | 'default'; +}; +``` + +In the following example, the console will sequentially output `'2'` and `'1'`, because `order` was set to `pre` when plugin2 called `modifyRsbuildConfig`. + +```ts +const plugin1 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig(() => console.log('1')); + }, +}); + +const plugin2 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig({ + handler: () => console.log('2'), + order: 'pre', + }); + }, +}); + +rsbuild.addPlugins([plugin1, plugin2]); +``` + +--- + ## Common Hooks ### modifyRsbuildConfig diff --git a/packages/document/docs/zh/plugins/dev/hooks.mdx b/packages/document/docs/zh/plugins/dev/hooks.mdx index 5c9ecfb8f3..ef661c0ee4 100644 --- a/packages/document/docs/zh/plugins/dev/hooks.mdx +++ b/packages/document/docs/zh/plugins/dev/hooks.mdx @@ -37,7 +37,7 @@ --- -## 执行顺序 +## Hooks 顺序 ### Dev Hooks @@ -77,6 +77,64 @@ --- +## 回调函数顺序 + +### 默认行为 + +如果多个插件注册了相同的 hook,那么 hook 的回调函数会按照注册时的顺序执行。 + +在以下例子中,控制台会依次输出 `'1'` 和 `'2'`: + +```ts +const plugin1 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig(() => console.log('1')); + }, +}); + +const plugin2 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig(() => console.log('2')); + }, +}); + +rsbuild.addPlugins([plugin1, plugin2]); +``` + +### order 字段 + +在注册 hook 时,可以通过 `order` 字段来声明 hook 的顺序。 + +```ts +type HookDescriptor any> = { + handler: T; + order: 'pre' | 'post' | 'default'; +}; +``` + +在以下例子中,控制台会依次输出 `'2'` 和 `'1'`,因为 plugin2 在调用 modifyRsbuildConfig 时设置了 order 为 `pre`。 + +```ts +const plugin1 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig(() => console.log('1')); + }, +}); + +const plugin2 = () => ({ + setup: (api) => { + api.modifyRsbuildConfig({ + handler: () => console.log('2'), + order: 'pre', + }); + }, +}); + +rsbuild.addPlugins([plugin1, plugin2]); +``` + +--- + ## Common Hooks ### modifyRsbuildConfig diff --git a/packages/shared/src/createHook.ts b/packages/shared/src/createHook.ts index f7be235434..0880f7a616 100644 --- a/packages/shared/src/createHook.ts +++ b/packages/shared/src/createHook.ts @@ -1,22 +1,42 @@ +import { isFunction } from './utils'; + +type HookOrder = 'pre' | 'post' | 'default'; + +export type HookDescriptor any> = { + handler: T; + order: HookOrder; +}; + export type AsyncHook any> = { - tap: (cb: Callback) => void; + tap: (cb: Callback | HookDescriptor) => void; call: (...args: Parameters) => Promise>; }; export function createAsyncHook< Callback extends (...args: any[]) => any, >(): AsyncHook { - const callbacks: Callback[] = []; + const preGroup: Callback[] = []; + const postGroup: Callback[] = []; + const defaultGroup: Callback[] = []; - const tap = (cb: Callback) => { - callbacks.push(cb); + const tap = (cb: Callback | HookDescriptor) => { + if (isFunction(cb)) { + defaultGroup.push(cb); + } else if (cb.order === 'pre') { + preGroup.push(cb.handler); + } else if (cb.order === 'post') { + postGroup.push(cb.handler); + } else { + defaultGroup.push(cb.handler); + } }; const call = async (...args: Parameters) => { const params = args.slice(0) as Parameters; + const callbacks = [...preGroup, ...defaultGroup, ...postGroup]; - for (const cb of callbacks) { - const result = await cb(...params); + for (const callback of callbacks) { + const result = await callback(...params); if (result !== undefined) { params[0] = result; diff --git a/packages/shared/src/types/plugin.ts b/packages/shared/src/types/plugin.ts index 87bbc01f6b..b0b4e1ddc5 100644 --- a/packages/shared/src/types/plugin.ts +++ b/packages/shared/src/types/plugin.ts @@ -28,6 +28,7 @@ import type { Configuration as WebpackConfig, } from 'webpack'; import type { ChainIdentifier } from '../chain'; +import type { HookDescriptor } from '../createHook'; export type ModifyRspackConfigFn = ( config: RspackConfig, @@ -143,6 +144,10 @@ export type GetRsbuildConfig = { (type: 'normalized'): NormalizedConfig; }; +type PluginHook any> = ( + options: T | HookDescriptor, +) => void; + /** * Define a generic Rsbuild plugin API that provider can extend as needed. */ @@ -150,16 +155,25 @@ export type RsbuildPluginAPI = { context: Readonly; isPluginExists: PluginStore['isPluginExists']; - onExit: (fn: OnExitFn) => void; - onAfterBuild: (fn: OnAfterBuildFn) => void; - onBeforeBuild: (fn: OnBeforeBuildFn) => void; - onDevCompileDone: (fn: OnDevCompileDoneFn) => void; - onAfterStartDevServer: (fn: OnAfterStartDevServerFn) => void; - onBeforeStartDevServer: (fn: OnBeforeStartDevServerFn) => void; - onAfterStartProdServer: (fn: OnAfterStartProdServerFn) => void; - onBeforeStartProdServer: (fn: OnBeforeStartProdServerFn) => void; - onAfterCreateCompiler: (fn: OnAfterCreateCompilerFn) => void; - onBeforeCreateCompiler: (fn: OnBeforeCreateCompilerFn) => void; + onExit: PluginHook; + onAfterBuild: PluginHook; + onBeforeBuild: PluginHook; + onDevCompileDone: PluginHook; + onAfterStartDevServer: PluginHook; + onBeforeStartDevServer: PluginHook; + onAfterStartProdServer: PluginHook; + onBeforeStartProdServer: PluginHook; + onAfterCreateCompiler: PluginHook; + onBeforeCreateCompiler: PluginHook; + + modifyRsbuildConfig: PluginHook; + modifyBundlerChain: PluginHook; + /** Only works when bundler is Rspack */ + modifyRspackConfig: PluginHook; + /** Only works when bundler is Webpack */ + modifyWebpackChain: PluginHook; + /** Only works when bundler is Webpack */ + modifyWebpackConfig: PluginHook; /** * Get the relative paths of generated HTML files. @@ -168,14 +182,4 @@ export type RsbuildPluginAPI = { getHTMLPaths: () => Record; getRsbuildConfig: GetRsbuildConfig; getNormalizedConfig: () => NormalizedConfig; - - modifyRsbuildConfig: (fn: ModifyRsbuildConfigFn) => void; - modifyBundlerChain: (fn: ModifyBundlerChainFn) => void; - - /** Only works when bundler is Rspack */ - modifyRspackConfig: (fn: ModifyRspackConfigFn) => void; - /** Only works when bundler is Webpack */ - modifyWebpackChain: (fn: ModifyWebpackChainFn) => void; - /** Only works when bundler is Webpack */ - modifyWebpackConfig: (fn: ModifyWebpackConfigFn) => void; }; diff --git a/packages/shared/tests/createAsyncHook.test.ts b/packages/shared/tests/createAsyncHook.test.ts index 77de01ad4f..fc9acb42d0 100644 --- a/packages/shared/tests/createAsyncHook.test.ts +++ b/packages/shared/tests/createAsyncHook.test.ts @@ -36,4 +36,53 @@ describe('createAsyncHook', () => { const result = await myHook.call(1); expect(result).toEqual([3]); }); + + test('should allow to specify hook order', async () => { + const myHook = createAsyncHook(); + const result: number[] = []; + + myHook.tap(() => { + result.push(1); + }); + myHook.tap({ + handler: () => { + result.push(2); + }, + order: 'post', + }); + myHook.tap({ + handler: () => { + result.push(3); + }, + order: 'post', + }); + myHook.tap({ + handler: () => { + result.push(4); + }, + order: 'default', + }); + myHook.tap({ + handler: () => { + result.push(5); + }, + order: 'pre', + }); + myHook.tap({ + handler: () => { + result.push(6); + }, + order: 'pre', + }); + myHook.tap({ + handler: () => { + result.push(7); + }, + order: 'default', + }); + + await myHook.call(); + + expect(result).toEqual([5, 6, 1, 4, 7, 2, 3]); + }); });